TokenAccessTests.mjs 43 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557
  1. import { expect } from 'chai'
  2. import async from 'async'
  3. import User from './helpers/User.mjs'
  4. import request from './helpers/request.js'
  5. import settings from '@overleaf/settings'
  6. import { db } from '../../../app/src/infrastructure/mongodb.mjs'
  7. import expectErrorResponse from './helpers/expectErrorResponse.mjs'
  8. import logger from '@overleaf/logger'
  9. import sinon from 'sinon'
  10. const tryEditorAccess = (user, projectId, test, callback) =>
  11. async.series(
  12. [
  13. cb =>
  14. user.request.get(`/project/${projectId}`, (error, response, body) => {
  15. if (error != null) {
  16. return cb(error)
  17. }
  18. test(response, body)
  19. cb()
  20. }),
  21. cb =>
  22. user.request.get(
  23. `/project/${projectId}/download/zip`,
  24. (error, response, body) => {
  25. if (error != null) {
  26. return cb(error)
  27. }
  28. test(response, body)
  29. cb()
  30. }
  31. ),
  32. ],
  33. callback
  34. )
  35. const tryReadOnlyTokenAccess = (
  36. user,
  37. token,
  38. testPageLoad,
  39. testFormPost,
  40. callback
  41. ) => {
  42. _doTryTokenAccess(
  43. `/read/${token}`,
  44. user,
  45. token,
  46. testPageLoad,
  47. testFormPost,
  48. callback
  49. )
  50. }
  51. const tryReadAndWriteTokenAccess = (
  52. user,
  53. token,
  54. testPageLoad,
  55. testFormPost,
  56. callback
  57. ) => {
  58. _doTryTokenAccess(
  59. `/${token}`,
  60. user,
  61. token,
  62. testPageLoad,
  63. testFormPost,
  64. callback
  65. )
  66. }
  67. const _doTryTokenAccess = (
  68. url,
  69. user,
  70. token,
  71. testPageLoad,
  72. testFormPost,
  73. callback
  74. ) => {
  75. user.request.get(url, (err, response, body) => {
  76. if (err) {
  77. return callback(err)
  78. }
  79. testPageLoad(response, body)
  80. if (!testFormPost) {
  81. return callback()
  82. }
  83. user.request.post(
  84. `${url}/grant`,
  85. { json: { token } },
  86. (err, response, body) => {
  87. if (err) {
  88. return callback(err)
  89. }
  90. testFormPost(response, body)
  91. callback()
  92. }
  93. )
  94. })
  95. }
  96. const tryReadOnlyTokenAccept = (
  97. user,
  98. token,
  99. testPageLoad,
  100. testFormPost,
  101. callback
  102. ) => {
  103. _doTryTokenAccept(
  104. `/read/${token}`,
  105. user,
  106. token,
  107. testPageLoad,
  108. testFormPost,
  109. callback
  110. )
  111. }
  112. const _doTryTokenAccept = (
  113. url,
  114. user,
  115. token,
  116. testPageLoad,
  117. testFormPost,
  118. callback
  119. ) => {
  120. user.request.get(url, (err, response, body) => {
  121. if (err) {
  122. return callback(err)
  123. }
  124. testPageLoad(response, body)
  125. if (!testFormPost) {
  126. return callback()
  127. }
  128. user.request.post(
  129. `${url}/grant`,
  130. { json: { token, confirmedByUser: true } },
  131. (err, response, body) => {
  132. if (err) {
  133. return callback(err)
  134. }
  135. testFormPost(response, body)
  136. callback()
  137. }
  138. )
  139. })
  140. }
  141. const tryContentAccess = (user, projcetId, test, callback) => {
  142. // The real-time service calls this end point to determine the user's
  143. // permissions.
  144. let userId
  145. if (user.id != null) {
  146. userId = user.id
  147. } else {
  148. userId = 'anonymous-user'
  149. }
  150. request.post(
  151. {
  152. url: `/project/${projcetId}/join`,
  153. auth: {
  154. user: settings.apis.web.user,
  155. pass: settings.apis.web.pass,
  156. sendImmediately: true,
  157. },
  158. json: {
  159. userId,
  160. },
  161. jar: false,
  162. },
  163. (error, response, body) => {
  164. if (error != null) {
  165. return callback(error)
  166. }
  167. test(response, body)
  168. callback()
  169. }
  170. )
  171. }
  172. const tryAnonContentAccess = (user, projectId, token, test, callback) => {
  173. // The real-time service calls this end point to determine the user's
  174. // permissions.
  175. let userId
  176. if (user.id != null) {
  177. userId = user.id
  178. } else {
  179. userId = 'anonymous-user'
  180. }
  181. request.post(
  182. {
  183. url: `/project/${projectId}/join`,
  184. auth: {
  185. user: settings.apis.web.user,
  186. pass: settings.apis.web.pass,
  187. sendImmediately: true,
  188. },
  189. json: {
  190. userId,
  191. anonymousAccessToken: token,
  192. },
  193. jar: false,
  194. },
  195. (error, response, body) => {
  196. if (error != null) {
  197. return callback(error)
  198. }
  199. test(response, body)
  200. callback()
  201. }
  202. )
  203. }
  204. const tryFetchProjectTokens = (user, projectId, callback) => {
  205. user.request.get(
  206. { url: `/project/${projectId}/tokens`, json: true },
  207. (error, response, body) => {
  208. if (error) {
  209. return callback(error)
  210. }
  211. callback(null, response, body)
  212. }
  213. )
  214. }
  215. const trySharingUpdatesPage = (user, projectId, test, callback) =>
  216. user.request.get(
  217. `/project/${projectId}/sharing-updates`,
  218. (error, response, body) => {
  219. if (error != null) {
  220. return callback(error)
  221. }
  222. test(response, body, projectId)
  223. callback()
  224. }
  225. )
  226. const trySharingUpdatesJoin = (user, projectId, test, callback) =>
  227. user.request.post(
  228. `/project/${projectId}/sharing-updates/join`,
  229. (error, response, body) => {
  230. if (error != null) {
  231. return callback(error)
  232. }
  233. test(response, body, projectId)
  234. callback()
  235. }
  236. )
  237. const trySharingUpdatesView = (user, projectId, test, callback) =>
  238. user.request.post(
  239. `/project/${projectId}/sharing-updates/view`,
  240. (error, response, body) => {
  241. if (error != null) {
  242. return callback(error)
  243. }
  244. test(response, body, projectId)
  245. callback()
  246. }
  247. )
  248. const expectRedirectToProject = (response, body, projectId) => {
  249. expect(response.statusCode).to.equal(302)
  250. expect(response.headers.location).to.equal(`/project/${projectId}`)
  251. }
  252. describe('TokenAccess', function () {
  253. beforeEach(function (done) {
  254. this.timeout(90000)
  255. this.owner = new User()
  256. this.other1 = new User()
  257. this.other2 = new User()
  258. this.anon = new User()
  259. this.siteAdmin = new User({ email: 'admin@example.com' })
  260. async.parallel(
  261. [
  262. cb =>
  263. this.siteAdmin.login(err => {
  264. if (err) {
  265. return cb(err)
  266. }
  267. this.siteAdmin.ensureAdmin(cb)
  268. }),
  269. cb => this.owner.login(cb),
  270. cb => this.other1.login(cb),
  271. cb => this.other2.login(cb),
  272. cb => this.anon.getCsrfToken(cb),
  273. ],
  274. done
  275. )
  276. })
  277. describe('no token-access', function () {
  278. beforeEach(function (done) {
  279. this.owner.createProject(
  280. `token-ro-test${Math.random()}`,
  281. (err, projectId) => {
  282. if (err != null) {
  283. return done(err)
  284. }
  285. this.projectId = projectId
  286. // Note, never made token-based,
  287. // thus no tokens
  288. done()
  289. }
  290. )
  291. })
  292. it('should deny access ', function (done) {
  293. async.series(
  294. [
  295. cb => {
  296. tryEditorAccess(
  297. this.other1,
  298. this.projectId,
  299. expectErrorResponse.restricted.html,
  300. cb
  301. )
  302. },
  303. cb => {
  304. tryContentAccess(
  305. this.other1,
  306. this.projectId,
  307. (response, body) => {
  308. expect(response.statusCode).to.equal(403)
  309. expect(body).to.equal('Forbidden')
  310. },
  311. cb
  312. )
  313. },
  314. ],
  315. done
  316. )
  317. })
  318. it('should deny access to access tokens', function (done) {
  319. tryFetchProjectTokens(this.other1, this.projectId, (error, response) => {
  320. expect(error).to.equal(null)
  321. expect(response.statusCode).to.equal(403)
  322. done()
  323. })
  324. })
  325. })
  326. describe('owner', function () {
  327. beforeEach(function (done) {
  328. this.projectName = `token-owner-test${Math.random()}`
  329. this.owner.createProject(this.projectName, (err, projectId) => {
  330. if (err != null) {
  331. return done(err)
  332. }
  333. this.projectId = projectId
  334. this.owner.makeTokenBased(this.projectId, err => {
  335. if (err != null) {
  336. return done(err)
  337. }
  338. this.owner.getProject(this.projectId, (err, project) => {
  339. if (err != null) {
  340. return done(err)
  341. }
  342. this.tokens = project.tokens
  343. done()
  344. })
  345. })
  346. })
  347. })
  348. it('should be able to fetch read-only and read-write tokens', function (done) {
  349. tryFetchProjectTokens(
  350. this.owner,
  351. this.projectId,
  352. (error, response, body) => {
  353. expect(error).to.equal(null)
  354. expect(response.statusCode).to.equal(200)
  355. expect(body).to.include({
  356. readOnly: this.tokens.readOnly,
  357. readAndWrite: this.tokens.readAndWrite,
  358. readAndWritePrefix: this.tokens.readAndWritePrefix,
  359. })
  360. expect(body.readOnlyHashPrefix).to.exist
  361. expect(body.readAndWriteHashPrefix).to.exist
  362. expect(Object.keys(body).length).to.equal(5)
  363. done()
  364. }
  365. )
  366. })
  367. })
  368. describe('read-only token', function () {
  369. beforeEach(function (done) {
  370. this.projectName = `token-ro-test${Math.random()}`
  371. this.owner.createProject(this.projectName, (err, projectId) => {
  372. if (err != null) {
  373. return done(err)
  374. }
  375. this.projectId = projectId
  376. this.owner.makeTokenBased(this.projectId, err => {
  377. if (err != null) {
  378. return done(err)
  379. }
  380. this.owner.getProject(this.projectId, (err, project) => {
  381. if (err != null) {
  382. return done(err)
  383. }
  384. this.tokens = project.tokens
  385. done()
  386. })
  387. })
  388. })
  389. })
  390. it('allow the user read-only access to the project', function (done) {
  391. async.series(
  392. [
  393. cb => {
  394. // deny access before token is used
  395. tryEditorAccess(
  396. this.other1,
  397. this.projectId,
  398. expectErrorResponse.restricted.html,
  399. cb
  400. )
  401. },
  402. cb => {
  403. // try token
  404. tryReadOnlyTokenAccess(
  405. this.other1,
  406. this.tokens.readOnly,
  407. (response, body) => {
  408. expect(response.statusCode).to.equal(200)
  409. },
  410. (response, body) => {
  411. expect(response.statusCode).to.equal(200)
  412. expect(body.requireAccept.projectName).to.equal(
  413. this.projectName
  414. )
  415. },
  416. cb
  417. )
  418. },
  419. cb => {
  420. // deny access before token is accepted
  421. tryEditorAccess(
  422. this.other1,
  423. this.projectId,
  424. expectErrorResponse.restricted.html,
  425. cb
  426. )
  427. },
  428. cb => {
  429. // accept token
  430. tryReadOnlyTokenAccept(
  431. this.other1,
  432. this.tokens.readOnly,
  433. (response, body) => {
  434. expect(response.statusCode).to.equal(200)
  435. },
  436. (response, body) => {
  437. expect(response.statusCode).to.equal(200)
  438. expect(body.redirect).to.equal(`/project/${this.projectId}`)
  439. expect(body.tokenAccessGranted).to.equal('readOnly')
  440. },
  441. cb
  442. )
  443. },
  444. cb => {
  445. // allow content access read-only
  446. tryContentAccess(
  447. this.other1,
  448. this.projectId,
  449. (response, body) => {
  450. expect(body.privilegeLevel).to.equal('readOnly')
  451. expect(body.isRestrictedUser).to.equal(true)
  452. expect(body.isTokenMember).to.equal(true)
  453. expect(body.isInvitedMember).to.equal(false)
  454. expect(body.project.owner).to.have.keys('_id')
  455. expect(body.project.owner).to.not.have.any.keys(
  456. 'email',
  457. 'first_name',
  458. 'last_name'
  459. )
  460. },
  461. cb
  462. )
  463. },
  464. cb => {
  465. tryEditorAccess(
  466. this.other1,
  467. this.projectId,
  468. (response, body) => {
  469. expect(response.statusCode).to.equal(200)
  470. },
  471. cb
  472. )
  473. },
  474. ],
  475. done
  476. )
  477. })
  478. it('should allow the user to fetch the read-only token', function (done) {
  479. async.series(
  480. [
  481. cb => {
  482. // accept token
  483. tryReadOnlyTokenAccept(
  484. this.other1,
  485. this.tokens.readOnly,
  486. (response, body) => {
  487. expect(response.statusCode).to.equal(200)
  488. },
  489. (response, body) => {
  490. expect(response.statusCode).to.equal(200)
  491. expect(body.redirect).to.equal(`/project/${this.projectId}`)
  492. expect(body.tokenAccessGranted).to.equal('readOnly')
  493. },
  494. cb
  495. )
  496. },
  497. cb => {
  498. tryFetchProjectTokens(
  499. this.other1,
  500. this.projectId,
  501. (error, response, body) => {
  502. expect(error).to.equal(null)
  503. expect(response.statusCode).to.equal(200)
  504. expect(body).to.include({ readOnly: this.tokens.readOnly })
  505. expect(body.readOnlyHashPrefix).to.exist
  506. cb()
  507. }
  508. )
  509. },
  510. ],
  511. done
  512. )
  513. })
  514. it('should redirect the admin to the project (with rw access)', function (done) {
  515. async.series(
  516. [
  517. cb => {
  518. // use token
  519. tryReadOnlyTokenAccess(
  520. this.siteAdmin,
  521. this.tokens.readOnly,
  522. (response, body) => {
  523. expect(response.statusCode).to.equal(200)
  524. },
  525. (response, body) => {
  526. expect(response.statusCode).to.equal(200)
  527. expect(body.redirect).to.equal(`/project/${this.projectId}`)
  528. },
  529. cb
  530. )
  531. },
  532. cb => {
  533. // allow content access read-and-write
  534. tryContentAccess(
  535. this.siteAdmin,
  536. this.projectId,
  537. (response, body) => {
  538. expect(body.privilegeLevel).to.equal('owner')
  539. expect(body.isRestrictedUser).to.equal(false)
  540. expect(body.isTokenMember).to.equal(false)
  541. expect(body.isInvitedMember).to.equal(false)
  542. },
  543. cb
  544. )
  545. },
  546. ],
  547. done
  548. )
  549. })
  550. describe('made private again', function () {
  551. beforeEach(function (done) {
  552. this.owner.makePrivate(this.projectId, () => setTimeout(done, 1000))
  553. })
  554. it('should not allow the user to access the project', function (done) {
  555. async.series(
  556. [
  557. // no access before token is used
  558. cb =>
  559. tryEditorAccess(
  560. this.other1,
  561. this.projectId,
  562. expectErrorResponse.restricted.html,
  563. cb
  564. ),
  565. // token goes nowhere
  566. cb =>
  567. tryReadOnlyTokenAccess(
  568. this.other1,
  569. this.tokens.readOnly,
  570. (response, body) => {
  571. expect(response.statusCode).to.equal(200)
  572. },
  573. (response, body) => {
  574. expect(response.statusCode).to.equal(404)
  575. },
  576. cb
  577. ),
  578. // still no access
  579. cb =>
  580. tryEditorAccess(
  581. this.other1,
  582. this.projectId,
  583. expectErrorResponse.restricted.html,
  584. cb
  585. ),
  586. cb =>
  587. tryContentAccess(
  588. this.other1,
  589. this.projectId,
  590. (response, body) => {
  591. expect(response.statusCode).to.equal(403)
  592. expect(body).to.equal('Forbidden')
  593. },
  594. cb
  595. ),
  596. ],
  597. done
  598. )
  599. })
  600. })
  601. it('should deny access to access tokens', function (done) {
  602. tryFetchProjectTokens(this.other1, this.projectId, (error, response) => {
  603. expect(error).to.equal(null)
  604. expect(response.statusCode).to.equal(403)
  605. done()
  606. })
  607. })
  608. })
  609. describe('anonymous read-only token', function () {
  610. beforeEach(function (done) {
  611. this.owner.createProject(
  612. `token-anon-ro-test${Math.random()}`,
  613. (err, projectId) => {
  614. if (err != null) {
  615. return done(err)
  616. }
  617. this.projectId = projectId
  618. this.owner.makeTokenBased(this.projectId, err => {
  619. if (err != null) {
  620. return done(err)
  621. }
  622. this.owner.getProject(this.projectId, (err, project) => {
  623. if (err != null) {
  624. return done(err)
  625. }
  626. this.tokens = project.tokens
  627. done()
  628. })
  629. })
  630. }
  631. )
  632. })
  633. it('should allow the user to access project via read-only token url', function (done) {
  634. async.series(
  635. [
  636. cb =>
  637. tryEditorAccess(
  638. this.anon,
  639. this.projectId,
  640. expectErrorResponse.restricted.html,
  641. cb
  642. ),
  643. cb =>
  644. tryReadOnlyTokenAccess(
  645. this.anon,
  646. this.tokens.readOnly,
  647. (response, body) => {
  648. expect(response.statusCode).to.equal(200)
  649. },
  650. (response, body) => {
  651. expect(response.statusCode).to.equal(200)
  652. expect(body.redirect).to.equal(`/project/${this.projectId}`)
  653. expect(body.grantAnonymousAccess).to.equal('readOnly')
  654. },
  655. cb
  656. ),
  657. cb =>
  658. tryEditorAccess(
  659. this.anon,
  660. this.projectId,
  661. (response, body) => {
  662. expect(response.statusCode).to.equal(200)
  663. },
  664. cb
  665. ),
  666. cb =>
  667. tryAnonContentAccess(
  668. this.anon,
  669. this.projectId,
  670. this.tokens.readOnly,
  671. (response, body) => {
  672. expect(body.privilegeLevel).to.equal('readOnly')
  673. expect(body.isRestrictedUser).to.equal(true)
  674. expect(body.isTokenMember).to.equal(false)
  675. expect(body.isInvitedMember).to.equal(false)
  676. expect(body.project.owner).to.have.keys('_id')
  677. expect(body.project.owner).to.not.have.any.keys(
  678. 'email',
  679. 'first_name',
  680. 'last_name'
  681. )
  682. },
  683. cb
  684. ),
  685. ],
  686. done
  687. )
  688. })
  689. it('should allow the anonymous user to fetch the read-only token', function (done) {
  690. async.series(
  691. [
  692. cb =>
  693. tryReadOnlyTokenAccess(
  694. this.anon,
  695. this.tokens.readOnly,
  696. (response, body) => {
  697. expect(response.statusCode).to.equal(200)
  698. },
  699. (response, body) => {
  700. expect(response.statusCode).to.equal(200)
  701. expect(body.redirect).to.equal(`/project/${this.projectId}`)
  702. expect(body.grantAnonymousAccess).to.equal('readOnly')
  703. },
  704. cb
  705. ),
  706. cb => {
  707. tryFetchProjectTokens(
  708. this.anon,
  709. this.projectId,
  710. (error, response, body) => {
  711. expect(error).to.equal(null)
  712. expect(response.statusCode).to.equal(200)
  713. expect(body).to.include({ readOnly: this.tokens.readOnly })
  714. expect(body.readOnlyHashPrefix).to.exist
  715. expect(Object.keys(body).length).to.equal(2)
  716. cb()
  717. }
  718. )
  719. },
  720. ],
  721. done
  722. )
  723. })
  724. describe('made private again', function () {
  725. beforeEach(function (done) {
  726. this.owner.makePrivate(this.projectId, () => setTimeout(done, 1000))
  727. })
  728. it('should deny access to project', function (done) {
  729. async.series(
  730. [
  731. cb =>
  732. tryEditorAccess(
  733. this.anon,
  734. this.projectId,
  735. expectErrorResponse.restricted.html,
  736. cb
  737. ),
  738. // should not allow the user to access read-only token
  739. cb =>
  740. tryReadOnlyTokenAccess(
  741. this.anon,
  742. this.tokens.readOnly,
  743. (response, body) => {
  744. expect(response.statusCode).to.equal(200)
  745. },
  746. (response, body) => {
  747. expect(response.statusCode).to.equal(404)
  748. },
  749. cb
  750. ),
  751. // still no access
  752. cb =>
  753. tryEditorAccess(
  754. this.anon,
  755. this.projectId,
  756. expectErrorResponse.restricted.html,
  757. cb
  758. ),
  759. // should not allow the user to join the project
  760. cb =>
  761. tryAnonContentAccess(
  762. this.anon,
  763. this.projectId,
  764. this.tokens.readOnly,
  765. (response, body) => {
  766. expect(response.statusCode).to.equal(403)
  767. expect(body).to.equal('Forbidden')
  768. },
  769. cb
  770. ),
  771. ],
  772. done
  773. )
  774. })
  775. it('should deny access to access tokens', function (done) {
  776. tryFetchProjectTokens(this.anon, this.projectId, (error, response) => {
  777. expect(error).to.equal(null)
  778. expect(response.statusCode).to.equal(403)
  779. done()
  780. })
  781. })
  782. })
  783. describe('link sharing disabled', function () {
  784. const previous = settings.disableLinkSharing
  785. let loggerStub
  786. beforeEach(function () {
  787. settings.disableLinkSharing = true
  788. loggerStub = sinon.spy(logger, 'error')
  789. })
  790. afterEach(function () {
  791. settings.disableLinkSharing = previous
  792. loggerStub.restore()
  793. })
  794. it('should deny access to project', function (done) {
  795. async.series(
  796. [
  797. cb =>
  798. tryEditorAccess(
  799. this.anon,
  800. this.projectId,
  801. expectErrorResponse.restricted.html,
  802. cb
  803. ),
  804. // should not allow the user to access read-only token
  805. cb =>
  806. tryReadOnlyTokenAccess(
  807. this.anon,
  808. this.tokens.readOnly,
  809. (response, body) => {
  810. // NOTE: This would be 404 when recreating the router. The Server Pro E2E tests cover this.
  811. expect(response.statusCode).to.equal(200)
  812. },
  813. (response, body) => {
  814. // NOTE: This would be 404 when recreating the router. The Server Pro E2E tests cover this.
  815. expect(response.statusCode).to.equal(500)
  816. expect(loggerStub).to.have.been.calledWithMatch(
  817. {
  818. err: { message: 'link sharing is disabled' },
  819. },
  820. '%s %s',
  821. 'POST',
  822. `/read/${this.tokens.readOnly}/grant`
  823. )
  824. },
  825. cb
  826. ),
  827. // still no access
  828. cb =>
  829. tryEditorAccess(
  830. this.anon,
  831. this.projectId,
  832. expectErrorResponse.restricted.html,
  833. cb
  834. ),
  835. // should not allow the user to join the project
  836. cb =>
  837. tryAnonContentAccess(
  838. this.anon,
  839. this.projectId,
  840. this.tokens.readOnly,
  841. (response, body) => {
  842. expect(response.statusCode).to.equal(403)
  843. expect(body).to.equal('Forbidden')
  844. },
  845. cb
  846. ),
  847. ],
  848. done
  849. )
  850. })
  851. it('should deny access to access tokens', function (done) {
  852. tryFetchProjectTokens(this.anon, this.projectId, (error, response) => {
  853. expect(error).to.equal(null)
  854. expect(response.statusCode).to.equal(403)
  855. done()
  856. })
  857. })
  858. it('should deny access to legacy public project', function (done) {
  859. async.series(
  860. [
  861. cb => this.owner.makePublic(this.projectId, 'readOnly', cb),
  862. cb =>
  863. tryAnonContentAccess(
  864. this.anon,
  865. this.projectId,
  866. this.tokens.readOnly,
  867. (response, body) => {
  868. expect(response.statusCode).to.equal(403)
  869. expect(body).to.equal('Forbidden')
  870. },
  871. cb
  872. ),
  873. ],
  874. done
  875. )
  876. })
  877. })
  878. })
  879. describe('anonymous read-and-write token, disabled (feature is deprecated)', function () {
  880. beforeEach(function (done) {
  881. this.owner.createProject(
  882. `token-anon-rw-test${Math.random()}`,
  883. (err, projectId) => {
  884. if (err != null) {
  885. return done(err)
  886. }
  887. this.projectId = projectId
  888. this.owner.makeTokenBased(this.projectId, err => {
  889. if (err != null) {
  890. return done(err)
  891. }
  892. this.owner.getProject(this.projectId, (err, project) => {
  893. if (err != null) {
  894. return done(err)
  895. }
  896. this.tokens = project.tokens
  897. done()
  898. })
  899. })
  900. }
  901. )
  902. })
  903. it('should not allow the user to access read-and-write token', function (done) {
  904. async.series(
  905. [
  906. cb =>
  907. tryEditorAccess(
  908. this.anon,
  909. this.projectId,
  910. expectErrorResponse.restricted.html,
  911. cb
  912. ),
  913. cb =>
  914. tryReadAndWriteTokenAccess(
  915. this.anon,
  916. this.tokens.readAndWrite,
  917. (response, body) => {
  918. expect(response.statusCode).to.equal(200)
  919. },
  920. (response, body) => {
  921. expect(response.statusCode).to.equal(200)
  922. expect(body).to.deep.equal({
  923. redirect: '/restricted',
  924. anonWriteAccessDenied: true,
  925. })
  926. },
  927. cb
  928. ),
  929. cb =>
  930. tryAnonContentAccess(
  931. this.anon,
  932. this.projectId,
  933. this.tokens.readAndWrite,
  934. (response, body) => {
  935. expect(response.statusCode).to.equal(403)
  936. expect(body).to.equal('Forbidden')
  937. },
  938. cb
  939. ),
  940. cb =>
  941. this.anon.login((err, response, body) => {
  942. expect(err).to.not.exist
  943. expect(response.statusCode).to.equal(200)
  944. expect(body.redir).to.equal(`/${this.tokens.readAndWrite}`)
  945. cb()
  946. }),
  947. ],
  948. done
  949. )
  950. })
  951. it('should deny access to access tokens', function (done) {
  952. tryFetchProjectTokens(this.anon, this.projectId, (error, response) => {
  953. expect(error).to.equal(null)
  954. expect(response.statusCode).to.equal(403)
  955. done()
  956. })
  957. })
  958. it('should require login if project does not exist', function (done) {
  959. async.series(
  960. [
  961. // delete project
  962. cb => {
  963. this.owner.deleteProject(this.projectId, cb)
  964. },
  965. cb =>
  966. tryReadAndWriteTokenAccess(
  967. this.anon,
  968. this.tokens.readAndWrite,
  969. (response, body) => {
  970. expect(response.statusCode).to.equal(200)
  971. },
  972. (response, body) => {
  973. expect(response.statusCode).to.equal(200)
  974. expect(body).to.deep.equal({
  975. redirect: '/restricted',
  976. anonWriteAccessDenied: true,
  977. })
  978. },
  979. cb
  980. ),
  981. cb =>
  982. this.anon.login((err, response, body) => {
  983. expect(err).to.not.exist
  984. expect(response.statusCode).to.equal(200)
  985. expect(body.redir).to.equal(`/${this.tokens.readAndWrite}`)
  986. cb()
  987. }),
  988. ],
  989. done
  990. )
  991. })
  992. it('should save URL hash in redirect', function (done) {
  993. const urlFragment = '#123456'
  994. const tokenWithUrlFragment = `${this.tokens.readAndWrite}${urlFragment}`
  995. async.series(
  996. [
  997. cb =>
  998. tryEditorAccess(
  999. this.anon,
  1000. this.projectId,
  1001. expectErrorResponse.restricted.html,
  1002. cb
  1003. ),
  1004. cb =>
  1005. this.anon.request.get(
  1006. tokenWithUrlFragment,
  1007. (err, response, body) => {
  1008. if (err) {
  1009. return cb(err)
  1010. }
  1011. expect(response.statusCode).to.equal(200)
  1012. this.anon.request.post(
  1013. `${this.tokens.readAndWrite}/grant`,
  1014. {
  1015. json: {
  1016. token: this.tokens.readAndWrite,
  1017. tokenHashPrefix: urlFragment,
  1018. },
  1019. },
  1020. (err, response, body) => {
  1021. if (err) {
  1022. return cb(err)
  1023. }
  1024. expect(response.statusCode).to.equal(200)
  1025. expect(body).to.deep.equal({
  1026. redirect: '/restricted',
  1027. anonWriteAccessDenied: true,
  1028. })
  1029. cb()
  1030. }
  1031. )
  1032. }
  1033. ),
  1034. cb =>
  1035. tryAnonContentAccess(
  1036. this.anon,
  1037. this.projectId,
  1038. this.tokens.readAndWrite,
  1039. (response, body) => {
  1040. expect(response.statusCode).to.equal(403)
  1041. expect(body).to.equal('Forbidden')
  1042. },
  1043. cb
  1044. ),
  1045. cb =>
  1046. this.anon.login((err, response, body) => {
  1047. expect(err).to.not.exist
  1048. expect(response.statusCode).to.equal(200)
  1049. expect(body.redir).to.equal(`/${tokenWithUrlFragment}`)
  1050. cb()
  1051. }),
  1052. ],
  1053. done
  1054. )
  1055. })
  1056. })
  1057. describe('private overleaf project', function () {
  1058. beforeEach(function (done) {
  1059. this.owner.createProject('overleaf-import', (err, projectId) => {
  1060. expect(err).not.to.exist
  1061. this.projectId = projectId
  1062. this.owner.makeTokenBased(this.projectId, err => {
  1063. expect(err).not.to.exist
  1064. this.owner.getProject(this.projectId, (err, project) => {
  1065. expect(err).not.to.exist
  1066. this.tokens = project.tokens
  1067. this.owner.makePrivate(this.projectId, () => {
  1068. db.projects.updateOne(
  1069. { _id: project._id },
  1070. {
  1071. $set: {
  1072. overleaf: { id: 1234 },
  1073. },
  1074. },
  1075. err => {
  1076. expect(err).not.to.exist
  1077. done()
  1078. }
  1079. )
  1080. })
  1081. })
  1082. })
  1083. })
  1084. })
  1085. it('should only allow the owner access to the project', function (done) {
  1086. async.series(
  1087. [
  1088. // should redirect to canonical path, when owner uses read-write token
  1089. cb =>
  1090. tryReadAndWriteTokenAccess(
  1091. this.owner,
  1092. this.tokens.readAndWrite,
  1093. (response, body) => {
  1094. expect(response.statusCode).to.equal(200)
  1095. },
  1096. (response, body) => {
  1097. expect(response.statusCode).to.equal(200)
  1098. expect(response.body.redirect).to.equal(
  1099. `/project/${this.projectId}`
  1100. )
  1101. expect(response.body.higherAccess).to.equal(true)
  1102. },
  1103. cb
  1104. ),
  1105. cb =>
  1106. tryEditorAccess(
  1107. this.owner,
  1108. this.projectId,
  1109. (response, body) => {
  1110. expect(response.statusCode).to.equal(200)
  1111. },
  1112. cb
  1113. ),
  1114. cb =>
  1115. tryContentAccess(
  1116. this.owner,
  1117. this.projectId,
  1118. (response, body) => {
  1119. expect(body.privilegeLevel).to.equal('owner')
  1120. },
  1121. cb
  1122. ),
  1123. // non-owner should be denied access
  1124. cb =>
  1125. tryContentAccess(
  1126. this.other2,
  1127. this.projectId,
  1128. (response, body) => {
  1129. expect(response.statusCode).to.equal(403)
  1130. expect(body).to.equal('Forbidden')
  1131. },
  1132. cb
  1133. ),
  1134. ],
  1135. done
  1136. )
  1137. })
  1138. })
  1139. describe('private project, with higher access', function () {
  1140. beforeEach(function (done) {
  1141. this.owner.createProject(
  1142. `higher-access-test-${Math.random()}`,
  1143. (err, projectId) => {
  1144. expect(err).not.to.exist
  1145. this.projectId = projectId
  1146. this.owner.addUserToProject(
  1147. this.projectId,
  1148. this.other1,
  1149. 'readAndWrite',
  1150. err => {
  1151. expect(err).not.to.exist
  1152. this.owner.makeTokenBased(this.projectId, err => {
  1153. expect(err).not.to.exist
  1154. this.owner.getProject(this.projectId, (err, project) => {
  1155. expect(err).not.to.exist
  1156. this.tokens = project.tokens
  1157. this.owner.makePrivate(this.projectId, () => {
  1158. setTimeout(done, 1000)
  1159. })
  1160. })
  1161. })
  1162. }
  1163. )
  1164. }
  1165. )
  1166. })
  1167. it('should allow the user access to the project', function (done) {
  1168. async.series(
  1169. [
  1170. // should redirect to canonical path, when user uses read-write token
  1171. cb =>
  1172. tryReadAndWriteTokenAccess(
  1173. this.other1,
  1174. this.tokens.readAndWrite,
  1175. (response, body) => {
  1176. expect(response.statusCode).to.equal(200)
  1177. },
  1178. (response, body) => {
  1179. expect(response.statusCode).to.equal(200)
  1180. expect(response.body.redirect).to.equal(
  1181. `/project/${this.projectId}`
  1182. )
  1183. expect(response.body.higherAccess).to.equal(true)
  1184. },
  1185. cb
  1186. ),
  1187. // should redirect to canonical path, when user uses read-only token
  1188. cb =>
  1189. tryReadOnlyTokenAccess(
  1190. this.other1,
  1191. this.tokens.readOnly,
  1192. (response, body) => {
  1193. expect(response.statusCode).to.equal(200)
  1194. },
  1195. (response, body) => {
  1196. expect(response.statusCode).to.equal(200)
  1197. expect(response.body.redirect).to.equal(
  1198. `/project/${this.projectId}`
  1199. )
  1200. expect(response.body.higherAccess).to.equal(true)
  1201. },
  1202. cb
  1203. ),
  1204. // should allow the user access to the project
  1205. cb =>
  1206. tryEditorAccess(
  1207. this.other1,
  1208. this.projectId,
  1209. (response, body) => {
  1210. expect(response.statusCode).to.equal(200)
  1211. },
  1212. cb
  1213. ),
  1214. // should allow user to join the project
  1215. cb =>
  1216. tryContentAccess(
  1217. this.other1,
  1218. this.projectId,
  1219. (response, body) => {
  1220. expect(body.privilegeLevel).to.equal('readAndWrite')
  1221. expect(body.isRestrictedUser).to.equal(false)
  1222. expect(body.isTokenMember).to.equal(false)
  1223. expect(body.isInvitedMember).to.equal(true)
  1224. },
  1225. cb
  1226. ),
  1227. // should not allow a different user to join the project
  1228. cb =>
  1229. tryEditorAccess(
  1230. this.other2,
  1231. this.projectId,
  1232. expectErrorResponse.restricted.html,
  1233. cb
  1234. ),
  1235. cb =>
  1236. tryContentAccess(
  1237. this.other2,
  1238. this.projectId,
  1239. (response, body) => {
  1240. expect(response.statusCode).to.equal(403)
  1241. expect(body).to.equal('Forbidden')
  1242. },
  1243. cb
  1244. ),
  1245. ],
  1246. done
  1247. )
  1248. })
  1249. })
  1250. describe('deleted project', function () {
  1251. beforeEach(function (done) {
  1252. this.owner.createProject(
  1253. `delete-test${Math.random()}`,
  1254. (err, projectId) => {
  1255. if (err != null) {
  1256. return done(err)
  1257. }
  1258. this.projectId = projectId
  1259. this.owner.makeTokenBased(this.projectId, err => {
  1260. if (err != null) {
  1261. return done(err)
  1262. }
  1263. this.owner.getProject(this.projectId, (err, project) => {
  1264. if (err != null) {
  1265. return done(err)
  1266. }
  1267. this.tokens = project.tokens
  1268. done()
  1269. })
  1270. })
  1271. }
  1272. )
  1273. })
  1274. it('should 404', function (done) {
  1275. async.series(
  1276. [
  1277. // delete project
  1278. cb => {
  1279. this.owner.deleteProject(this.projectId, cb)
  1280. },
  1281. cb => {
  1282. // use read-only token
  1283. tryReadOnlyTokenAccess(
  1284. this.other1,
  1285. this.tokens.readOnly,
  1286. (response, body) => {
  1287. expect(response.statusCode).to.equal(200)
  1288. },
  1289. (response, body) => {
  1290. expect(response.statusCode).to.equal(404)
  1291. },
  1292. cb
  1293. )
  1294. },
  1295. cb => {
  1296. // use read-write token
  1297. tryReadAndWriteTokenAccess(
  1298. this.other1,
  1299. this.tokens.readAndWrite,
  1300. (response, body) => {
  1301. expect(response.statusCode).to.equal(200)
  1302. },
  1303. (response, body) => {
  1304. expect(response.statusCode).to.equal(404)
  1305. },
  1306. cb
  1307. )
  1308. },
  1309. ],
  1310. done
  1311. )
  1312. })
  1313. it('should deny access to access tokens with a 404', function (done) {
  1314. async.series(
  1315. [
  1316. // delete project
  1317. cb => {
  1318. this.owner.deleteProject(this.projectId, cb)
  1319. },
  1320. cb => {
  1321. // use read-only token
  1322. tryReadOnlyTokenAccess(
  1323. this.other1,
  1324. this.tokens.readOnly,
  1325. (response, body) => {
  1326. expect(response.statusCode).to.equal(200)
  1327. },
  1328. (response, body) => {
  1329. expect(response.statusCode).to.equal(404)
  1330. },
  1331. cb
  1332. )
  1333. },
  1334. cb => {
  1335. tryFetchProjectTokens(
  1336. this.other1,
  1337. this.projectId,
  1338. (error, response) => {
  1339. expect(error).to.equal(null)
  1340. expect(response.statusCode).to.equal(404)
  1341. cb()
  1342. }
  1343. )
  1344. },
  1345. ],
  1346. done
  1347. )
  1348. })
  1349. })
  1350. describe('sharing updates consent page for read-and-write token deprecation', function () {
  1351. describe('not a member of the project', function () {
  1352. beforeEach(function (done) {
  1353. this.projectName = `token-link-sharing-changes${Math.random()}`
  1354. this.owner.createProject(this.projectName, (err, projectId) => {
  1355. if (err != null) {
  1356. return done(err)
  1357. }
  1358. this.projectId = projectId
  1359. this.owner.makeTokenBased(this.projectId, err => {
  1360. if (err != null) {
  1361. return done(err)
  1362. }
  1363. this.owner.getProject(this.projectId, (err, project) => {
  1364. if (err != null) {
  1365. return done(err)
  1366. }
  1367. this.tokens = project.tokens
  1368. done()
  1369. })
  1370. })
  1371. })
  1372. })
  1373. it('should deny access', function (done) {
  1374. async.series(
  1375. [
  1376. cb => {
  1377. trySharingUpdatesPage(
  1378. this.other1,
  1379. this.projectId,
  1380. expectErrorResponse.restricted.html,
  1381. cb
  1382. )
  1383. },
  1384. cb => {
  1385. trySharingUpdatesJoin(
  1386. this.other1,
  1387. this.projectId,
  1388. expectErrorResponse.restricted.html,
  1389. cb
  1390. )
  1391. },
  1392. cb => {
  1393. trySharingUpdatesView(
  1394. this.other1,
  1395. this.projectId,
  1396. expectErrorResponse.restricted.html,
  1397. cb
  1398. )
  1399. },
  1400. ],
  1401. done
  1402. )
  1403. })
  1404. })
  1405. describe('read-only token member of project', function () {
  1406. beforeEach(function (done) {
  1407. this.projectName = `token-link-sharing-changes${Math.random()}`
  1408. this.owner.createProject(this.projectName, (err, projectId) => {
  1409. if (err != null) {
  1410. return done(err)
  1411. }
  1412. this.projectId = projectId
  1413. this.owner.makeTokenBased(this.projectId, err => {
  1414. if (err != null) {
  1415. return done(err)
  1416. }
  1417. this.owner.getProject(this.projectId, (err, project) => {
  1418. if (err != null) {
  1419. return done(err)
  1420. }
  1421. this.tokens = project.tokens
  1422. tryReadOnlyTokenAccept(
  1423. this.other1,
  1424. this.tokens.readOnly,
  1425. (response, body) => {
  1426. expect(response.statusCode).to.equal(200)
  1427. },
  1428. (response, body) => {
  1429. expect(response.statusCode).to.equal(200)
  1430. expect(body.redirect).to.equal(`/project/${this.projectId}`)
  1431. expect(body.tokenAccessGranted).to.equal('readOnly')
  1432. },
  1433. done
  1434. )
  1435. })
  1436. })
  1437. })
  1438. })
  1439. it('should redirect to project', function (done) {
  1440. async.series(
  1441. [
  1442. cb => {
  1443. trySharingUpdatesPage(
  1444. this.other1,
  1445. this.projectId,
  1446. expectRedirectToProject,
  1447. cb
  1448. )
  1449. },
  1450. cb => {
  1451. trySharingUpdatesJoin(
  1452. this.other1,
  1453. this.projectId,
  1454. expectRedirectToProject,
  1455. cb
  1456. )
  1457. },
  1458. cb => {
  1459. trySharingUpdatesView(
  1460. this.other1,
  1461. this.projectId,
  1462. expectRedirectToProject,
  1463. cb
  1464. )
  1465. },
  1466. cb => {
  1467. // allow content access read-only
  1468. tryContentAccess(
  1469. this.other1,
  1470. this.projectId,
  1471. (response, body) => {
  1472. expect(body.privilegeLevel).to.equal('readOnly')
  1473. expect(body.isRestrictedUser).to.equal(true)
  1474. expect(body.isTokenMember).to.equal(true)
  1475. expect(body.isInvitedMember).to.equal(false)
  1476. expect(body.project.owner).to.have.keys('_id')
  1477. expect(body.project.owner).to.not.have.any.keys(
  1478. 'email',
  1479. 'first_name',
  1480. 'last_name'
  1481. )
  1482. },
  1483. cb
  1484. )
  1485. },
  1486. cb => {
  1487. tryEditorAccess(
  1488. this.other1,
  1489. this.projectId,
  1490. (response, body) => {
  1491. expect(response.statusCode).to.equal(200)
  1492. },
  1493. cb
  1494. )
  1495. },
  1496. ],
  1497. done
  1498. )
  1499. })
  1500. })
  1501. })
  1502. })