TokenAccessController.js 16 KB

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