TokenAccessController.mjs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610
  1. import AuthenticationController from '../Authentication/AuthenticationController.js'
  2. import SessionManager from '../Authentication/SessionManager.js'
  3. import TokenAccessHandler from './TokenAccessHandler.js'
  4. import Errors from '../Errors/Errors.js'
  5. import logger from '@overleaf/logger'
  6. import OError from '@overleaf/o-error'
  7. import { expressify } from '@overleaf/promise-utils'
  8. import AuthorizationManager from '../Authorization/AuthorizationManager.js'
  9. import PrivilegeLevels from '../Authorization/PrivilegeLevels.js'
  10. import ProjectAuditLogHandler from '../Project/ProjectAuditLogHandler.js'
  11. import CollaboratorsInviteHandler from '../Collaborators/CollaboratorsInviteHandler.mjs'
  12. import CollaboratorsHandler from '../Collaborators/CollaboratorsHandler.js'
  13. import EditorRealTimeController from '../Editor/EditorRealTimeController.js'
  14. import CollaboratorsGetter from '../Collaborators/CollaboratorsGetter.js'
  15. import ProjectGetter from '../Project/ProjectGetter.js'
  16. import AsyncFormHelper from '../Helpers/AsyncFormHelper.js'
  17. import AnalyticsManager from '../Analytics/AnalyticsManager.js'
  18. import { canRedirectToAdminDomain } from '../Helpers/AdminAuthorizationHelper.js'
  19. import { getSafeAdminDomainRedirect } from '../Helpers/UrlHelper.js'
  20. import UserGetter from '../User/UserGetter.js'
  21. import Settings from '@overleaf/settings'
  22. import LimitationsManager from '../Subscription/LimitationsManager.js'
  23. import SplitTestHandler from '../SplitTests/SplitTestHandler.js'
  24. const orderedPrivilegeLevels = [
  25. PrivilegeLevels.NONE,
  26. PrivilegeLevels.READ_ONLY,
  27. PrivilegeLevels.REVIEW,
  28. PrivilegeLevels.READ_AND_WRITE,
  29. PrivilegeLevels.OWNER,
  30. ]
  31. async function _userAlreadyHasHigherPrivilege(userId, projectId, tokenType) {
  32. if (!Object.values(TokenAccessHandler.TOKEN_TYPES).includes(tokenType)) {
  33. throw new Error('bad token type')
  34. }
  35. if (!userId) {
  36. return false
  37. }
  38. const privilegeLevel =
  39. await AuthorizationManager.promises.getPrivilegeLevelForProject(
  40. userId,
  41. projectId
  42. )
  43. return (
  44. orderedPrivilegeLevels.indexOf(privilegeLevel) >=
  45. orderedPrivilegeLevels.indexOf(tokenType)
  46. )
  47. }
  48. const makePostUrl = token => {
  49. if (TokenAccessHandler.isReadAndWriteToken(token)) {
  50. return `/${token}/grant`
  51. } else if (TokenAccessHandler.isReadOnlyToken(token)) {
  52. return `/read/${token}/grant`
  53. } else {
  54. throw new Error('invalid token type')
  55. }
  56. }
  57. async function _handleV1Project(token, userId) {
  58. if (!userId) {
  59. return { v1Import: { status: 'mustLogin' } }
  60. } else {
  61. const docInfo = await TokenAccessHandler.promises.getV1DocInfo(
  62. token,
  63. userId
  64. )
  65. // This should not happen anymore, but it does show
  66. // a nice "contact support" message, so it can stay
  67. if (!docInfo) {
  68. return { v1Import: { status: 'cannotImport' } }
  69. }
  70. if (!docInfo.exists) {
  71. return null
  72. }
  73. if (docInfo.exported) {
  74. return null
  75. }
  76. return {
  77. v1Import: {
  78. status: 'canDownloadZip',
  79. projectId: token,
  80. hasOwner: docInfo.has_owner,
  81. name: docInfo.name || 'Untitled',
  82. brandInfo: docInfo.brand_info,
  83. },
  84. }
  85. }
  86. }
  87. async function _isOverleafStaff(userId) {
  88. const emails = await UserGetter.promises.getUserConfirmedEmails(userId)
  89. const adminDomains = Settings.adminDomains ?? []
  90. return emails.some(email =>
  91. adminDomains.some(adminDomain => email.email.endsWith(`@${adminDomain}`))
  92. )
  93. }
  94. async function tokenAccessPage(req, res, next) {
  95. const { token } = req.params
  96. if (!TokenAccessHandler.isValidToken(token)) {
  97. return next(new Errors.NotFoundError())
  98. }
  99. try {
  100. if (TokenAccessHandler.isReadOnlyToken(token)) {
  101. const docPublishedInfo =
  102. await TokenAccessHandler.promises.getV1DocPublishedInfo(token)
  103. if (docPublishedInfo.allow === false) {
  104. return res.redirect(302, docPublishedInfo.published_path)
  105. }
  106. }
  107. // Populates splitTestVariants with a value for the split test name and allows
  108. // Pug to read it
  109. await SplitTestHandler.promises.getAssignment(
  110. req,
  111. res,
  112. 'bs5-misc-pages-b2c'
  113. )
  114. res.render('project/token/access-react', {
  115. postUrl: makePostUrl(token),
  116. })
  117. } catch (err) {
  118. return next(
  119. OError.tag(err, 'error while rendering token access page', { token })
  120. )
  121. }
  122. }
  123. async function checkAndGetProjectOrResponseAction(
  124. tokenType,
  125. token,
  126. userId,
  127. tokenHashPrefix,
  128. req,
  129. res,
  130. next
  131. ) {
  132. const isAnonymousUser = !userId
  133. if (
  134. isAnonymousUser &&
  135. tokenType === TokenAccessHandler.TOKEN_TYPES.READ_AND_WRITE &&
  136. !TokenAccessHandler.ANONYMOUS_READ_AND_WRITE_ENABLED
  137. ) {
  138. logger.warn('[TokenAccess] deny anonymous read-and-write token access')
  139. let projectUrlWithToken = TokenAccessHandler.makeTokenUrl(token)
  140. if (tokenHashPrefix && tokenHashPrefix.startsWith('#')) {
  141. projectUrlWithToken += `${tokenHashPrefix}`
  142. }
  143. AuthenticationController.setRedirectInSession(req, projectUrlWithToken)
  144. return [
  145. null,
  146. () => {
  147. res.json({
  148. redirect: '/restricted',
  149. anonWriteAccessDenied: true,
  150. })
  151. },
  152. { action: 'denied anonymous read-and-write token access' },
  153. ]
  154. }
  155. // Try to get the project, and/or an alternative action to take.
  156. // Returns a tuple of [project, action]
  157. const project = await TokenAccessHandler.promises.getProjectByToken(
  158. tokenType,
  159. token
  160. )
  161. if (!project) {
  162. if (Settings.overleaf) {
  163. const v1ImportData = await _handleV1Project(token, userId)
  164. return [
  165. null,
  166. () => {
  167. if (v1ImportData) {
  168. res.json(v1ImportData)
  169. } else {
  170. res.sendStatus(404)
  171. }
  172. },
  173. { action: v1ImportData ? 'import v1' : '404' },
  174. ]
  175. } else {
  176. return [null, null, { action: '404' }]
  177. }
  178. }
  179. const projectId = project._id
  180. const tokenAccessEnabled =
  181. TokenAccessHandler.tokenAccessEnabledForProject(project)
  182. if (isAnonymousUser && tokenAccessEnabled) {
  183. if (tokenType === TokenAccessHandler.TOKEN_TYPES.READ_AND_WRITE) {
  184. if (TokenAccessHandler.ANONYMOUS_READ_AND_WRITE_ENABLED) {
  185. logger.debug({ projectId }, 'granting read-write anonymous access')
  186. TokenAccessHandler.grantSessionTokenAccess(req, projectId, token)
  187. return [
  188. null,
  189. () => {
  190. res.json({
  191. redirect: `/project/${projectId}`,
  192. grantAnonymousAccess: tokenType,
  193. })
  194. },
  195. { projectId, action: 'granting read-write anonymous access' },
  196. ]
  197. } else {
  198. // anonymous read-and-write token access should have been denied already
  199. throw new Error(
  200. 'unreachable: anonymous read-and-write token access bug'
  201. )
  202. }
  203. } else if (tokenType === TokenAccessHandler.TOKEN_TYPES.READ_ONLY) {
  204. logger.debug({ projectId }, 'granting read-only anonymous access')
  205. TokenAccessHandler.grantSessionTokenAccess(req, projectId, token)
  206. return [
  207. null,
  208. () => {
  209. res.json({
  210. redirect: `/project/${projectId}`,
  211. grantAnonymousAccess: tokenType,
  212. })
  213. },
  214. { projectId, action: 'granting read-only anonymous access' },
  215. ]
  216. } else {
  217. throw new Error('unreachable')
  218. }
  219. }
  220. const userHasPrivilege = await _userAlreadyHasHigherPrivilege(
  221. userId,
  222. projectId,
  223. tokenType
  224. )
  225. if (userHasPrivilege) {
  226. return [
  227. null,
  228. () => {
  229. res.json({ redirect: `/project/${project._id}`, higherAccess: true })
  230. },
  231. { projectId, action: 'user already has higher or same privilege' },
  232. ]
  233. }
  234. // Handle admin redirect
  235. // If the project owner is an internal staff (using @overleaf.com email),
  236. // the admin will join the project "for real".
  237. // If the project owner is a external user
  238. // the admin will be redirect to admin domain to view the project.
  239. if (canRedirectToAdminDomain(SessionManager.getSessionUser(req.session))) {
  240. const isProjectOwnerOverleafStaff = await _isOverleafStaff(
  241. project.owner_ref
  242. )
  243. if (isProjectOwnerOverleafStaff) {
  244. logger.warn(
  245. { projectId, userId },
  246. 'letting admin user join staff project'
  247. )
  248. } else {
  249. let projectUrlWithToken = TokenAccessHandler.makeTokenUrl(token)
  250. if (tokenHashPrefix && tokenHashPrefix.startsWith('#')) {
  251. projectUrlWithToken += `${tokenHashPrefix}`
  252. }
  253. return [
  254. null,
  255. () =>
  256. res.json({
  257. redirect: getSafeAdminDomainRedirect(projectUrlWithToken),
  258. }),
  259. { projectId, action: 'redirect admin user to admin domain' },
  260. ]
  261. }
  262. }
  263. if (!tokenAccessEnabled) {
  264. return [
  265. null,
  266. () => {
  267. next(new Errors.NotFoundError())
  268. },
  269. { projectId, action: 'token access not enabled' },
  270. ]
  271. }
  272. return [project, null, { projectId, action: 'continue' }]
  273. }
  274. async function grantTokenAccessReadAndWrite(req, res, next) {
  275. const { token } = req.params
  276. const { confirmedByUser, tokenHashPrefix } = req.body
  277. const userId = SessionManager.getLoggedInUserId(req.session)
  278. if (!TokenAccessHandler.isReadAndWriteToken(token)) {
  279. return res.sendStatus(400)
  280. }
  281. const tokenType = TokenAccessHandler.TOKEN_TYPES.READ_AND_WRITE
  282. try {
  283. const [project, action, logData] = await checkAndGetProjectOrResponseAction(
  284. tokenType,
  285. token,
  286. userId,
  287. tokenHashPrefix,
  288. req,
  289. res,
  290. next
  291. )
  292. TokenAccessHandler.checkTokenHashPrefix(
  293. token,
  294. tokenHashPrefix,
  295. tokenType,
  296. userId,
  297. logData
  298. )
  299. if (action) {
  300. return action()
  301. }
  302. if (!project) {
  303. return next(new Errors.NotFoundError())
  304. }
  305. if (!confirmedByUser) {
  306. return res.json({
  307. requireAccept: {
  308. projectName: project.name,
  309. },
  310. })
  311. }
  312. const pendingEditor =
  313. !(await LimitationsManager.promises.canAcceptEditCollaboratorInvite(
  314. project._id
  315. ))
  316. await ProjectAuditLogHandler.promises.addEntry(
  317. project._id,
  318. 'accept-via-link-sharing',
  319. userId,
  320. req.ip,
  321. {
  322. privileges: pendingEditor ? 'readOnly' : 'readAndWrite',
  323. ...(pendingEditor && { pendingEditor: true }),
  324. }
  325. )
  326. AnalyticsManager.recordEventForUserInBackground(userId, 'project-joined', {
  327. role: pendingEditor
  328. ? PrivilegeLevels.READ_ONLY
  329. : PrivilegeLevels.READ_AND_WRITE,
  330. ownerId: project.owner_ref.toString(),
  331. source: 'link-sharing',
  332. mode: pendingEditor ? 'view' : 'edit',
  333. projectId: project._id.toString(),
  334. ...(pendingEditor && { pendingEditor: true }),
  335. })
  336. await CollaboratorsHandler.promises.addUserIdToProject(
  337. project._id,
  338. undefined,
  339. userId,
  340. pendingEditor
  341. ? PrivilegeLevels.READ_ONLY
  342. : PrivilegeLevels.READ_AND_WRITE,
  343. { pendingEditor }
  344. )
  345. // remove pending invite and notification
  346. const userEmails = await UserGetter.promises.getUserConfirmedEmails(userId)
  347. await CollaboratorsInviteHandler.promises.revokeInviteForUser(
  348. project._id,
  349. userEmails
  350. )
  351. // Should be a noop if the user is already a member,
  352. // and would redirect transparently into the project.
  353. EditorRealTimeController.emitToRoom(
  354. project._id,
  355. 'project:membership:changed',
  356. { members: true, invites: true }
  357. )
  358. return res.json({
  359. redirect: `/project/${project._id}`,
  360. })
  361. } catch (err) {
  362. return next(
  363. OError.tag(
  364. err,
  365. 'error while trying to grant read-and-write token access',
  366. { token }
  367. )
  368. )
  369. }
  370. }
  371. async function grantTokenAccessReadOnly(req, res, next) {
  372. const { token } = req.params
  373. const { confirmedByUser, tokenHashPrefix } = req.body
  374. const userId = SessionManager.getLoggedInUserId(req.session)
  375. if (!TokenAccessHandler.isReadOnlyToken(token)) {
  376. return res.sendStatus(400)
  377. }
  378. const tokenType = TokenAccessHandler.TOKEN_TYPES.READ_ONLY
  379. const docPublishedInfo =
  380. await TokenAccessHandler.promises.getV1DocPublishedInfo(token)
  381. if (docPublishedInfo.allow === false) {
  382. return res.json({ redirect: docPublishedInfo.published_path })
  383. }
  384. try {
  385. const [project, action, logData] = await checkAndGetProjectOrResponseAction(
  386. tokenType,
  387. token,
  388. userId,
  389. tokenHashPrefix,
  390. req,
  391. res,
  392. next
  393. )
  394. TokenAccessHandler.checkTokenHashPrefix(
  395. token,
  396. tokenHashPrefix,
  397. tokenType,
  398. userId,
  399. logData
  400. )
  401. if (action) {
  402. return action()
  403. }
  404. if (!project) {
  405. return next(new Errors.NotFoundError())
  406. }
  407. if (!confirmedByUser) {
  408. return res.json({
  409. requireAccept: {
  410. projectName: project.name,
  411. },
  412. })
  413. }
  414. if (!project.tokenAccessReadOnly_refs.some(id => id.equals(userId))) {
  415. await ProjectAuditLogHandler.promises.addEntry(
  416. project._id,
  417. 'join-via-token',
  418. userId,
  419. req.ip,
  420. { privileges: 'readOnly' }
  421. )
  422. }
  423. await TokenAccessHandler.promises.addReadOnlyUserToProject(
  424. userId,
  425. project._id,
  426. project.owner_ref
  427. )
  428. return res.json({
  429. redirect: `/project/${project._id}`,
  430. tokenAccessGranted: tokenType,
  431. })
  432. } catch (err) {
  433. return next(
  434. OError.tag(err, 'error while trying to grant read-only token access', {
  435. token,
  436. })
  437. )
  438. }
  439. }
  440. async function ensureUserCanUseSharingUpdatesConsentPage(req, res, next) {
  441. const { Project_id: projectId } = req.params
  442. const userId = SessionManager.getLoggedInUserId(req.session)
  443. const project = await ProjectGetter.promises.getProject(projectId, {
  444. owner_ref: 1,
  445. })
  446. if (!project) {
  447. throw new Errors.NotFoundError()
  448. }
  449. const isReadWriteTokenMember =
  450. await CollaboratorsGetter.promises.userIsReadWriteTokenMember(
  451. userId,
  452. projectId
  453. )
  454. if (!isReadWriteTokenMember) {
  455. // If the user is not a read write token member, there are no actions to take
  456. return AsyncFormHelper.redirect(req, res, `/project/${projectId}`)
  457. }
  458. const isReadWriteMember =
  459. await CollaboratorsGetter.promises.isUserInvitedReadWriteMemberOfProject(
  460. userId,
  461. projectId
  462. )
  463. if (isReadWriteMember) {
  464. // If the user is already an invited editor, the actions don't make sense
  465. return AsyncFormHelper.redirect(req, res, `/project/${projectId}`)
  466. }
  467. next()
  468. }
  469. async function sharingUpdatesConsent(req, res, next) {
  470. const { Project_id: projectId } = req.params
  471. AnalyticsManager.recordEventForSession(req.session, 'notification-prompt', {
  472. page: req.path,
  473. name: 'link-sharing-collaborator',
  474. })
  475. res.render('project/token/sharing-updates', {
  476. projectId,
  477. })
  478. }
  479. async function moveReadWriteToCollaborators(req, res, next) {
  480. const { Project_id: projectId } = req.params
  481. const userId = SessionManager.getLoggedInUserId(req.session)
  482. const project = await ProjectGetter.promises.getProject(projectId, {
  483. owner_ref: 1,
  484. })
  485. const isInvitedMember =
  486. await CollaboratorsGetter.promises.isUserInvitedMemberOfProject(
  487. userId,
  488. projectId
  489. )
  490. const pendingEditor =
  491. !(await LimitationsManager.promises.canAcceptEditCollaboratorInvite(
  492. project._id
  493. ))
  494. await ProjectAuditLogHandler.promises.addEntry(
  495. projectId,
  496. 'accept-via-link-sharing',
  497. userId,
  498. req.ip,
  499. {
  500. privileges: pendingEditor
  501. ? PrivilegeLevels.READ_ONLY
  502. : PrivilegeLevels.READ_AND_WRITE,
  503. tokenMember: true,
  504. invitedMember: isInvitedMember,
  505. ...(pendingEditor && { pendingEditor: true }),
  506. }
  507. )
  508. if (isInvitedMember) {
  509. // Read only invited viewer who is gaining edit access via link sharing
  510. await TokenAccessHandler.promises.removeReadAndWriteUserFromProject(
  511. userId,
  512. projectId
  513. )
  514. await CollaboratorsHandler.promises.setCollaboratorPrivilegeLevel(
  515. projectId,
  516. userId,
  517. pendingEditor
  518. ? PrivilegeLevels.READ_ONLY
  519. : PrivilegeLevels.READ_AND_WRITE,
  520. { pendingEditor }
  521. )
  522. } else {
  523. // Normal case, not invited, joining via link sharing
  524. await TokenAccessHandler.promises.removeReadAndWriteUserFromProject(
  525. userId,
  526. projectId
  527. )
  528. await CollaboratorsHandler.promises.addUserIdToProject(
  529. projectId,
  530. undefined,
  531. userId,
  532. pendingEditor
  533. ? PrivilegeLevels.READ_ONLY
  534. : PrivilegeLevels.READ_AND_WRITE,
  535. { pendingEditor }
  536. )
  537. }
  538. EditorRealTimeController.emitToRoom(projectId, 'project:membership:changed', {
  539. members: true,
  540. })
  541. res.sendStatus(204)
  542. }
  543. async function moveReadWriteToReadOnly(req, res, next) {
  544. const { Project_id: projectId } = req.params
  545. const userId = SessionManager.getLoggedInUserId(req.session)
  546. await ProjectAuditLogHandler.promises.addEntry(
  547. projectId,
  548. 'readonly-via-sharing-updates',
  549. userId,
  550. req.ip
  551. )
  552. await TokenAccessHandler.promises.moveReadAndWriteUserToReadOnly(
  553. userId,
  554. projectId
  555. )
  556. res.sendStatus(204)
  557. }
  558. export default {
  559. READ_ONLY_TOKEN_PATTERN: TokenAccessHandler.READ_ONLY_TOKEN_PATTERN,
  560. READ_AND_WRITE_TOKEN_PATTERN: TokenAccessHandler.READ_AND_WRITE_TOKEN_PATTERN,
  561. tokenAccessPage: expressify(tokenAccessPage),
  562. grantTokenAccessReadOnly: expressify(grantTokenAccessReadOnly),
  563. grantTokenAccessReadAndWrite: expressify(grantTokenAccessReadAndWrite),
  564. ensureUserCanUseSharingUpdatesConsentPage: expressify(
  565. ensureUserCanUseSharingUpdatesConsentPage
  566. ),
  567. sharingUpdatesConsent: expressify(sharingUpdatesConsent),
  568. moveReadWriteToCollaborators: expressify(moveReadWriteToCollaborators),
  569. moveReadWriteToReadOnly: expressify(moveReadWriteToReadOnly),
  570. }