TokenAccessController.js 17 KB

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