TokenAccessControllerTests.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621
  1. const SandboxedModule = require('sandboxed-module')
  2. const sinon = require('sinon')
  3. const { expect } = require('chai')
  4. const { ObjectId } = require('mongodb')
  5. const MockRequest = require('../helpers/MockRequest')
  6. const MockResponse = require('../helpers/MockResponse')
  7. const PrivilegeLevels = require('../../../../app/src/Features/Authorization/PrivilegeLevels')
  8. const MODULE_PATH =
  9. '../../../../app/src/Features/TokenAccess/TokenAccessController'
  10. describe('TokenAccessController', function () {
  11. beforeEach(function () {
  12. this.token = 'abc123'
  13. this.user = { _id: new ObjectId() }
  14. this.project = {
  15. _id: new ObjectId(),
  16. tokenAccessReadAndWrite_refs: [],
  17. tokenAccessReadOnly_refs: [],
  18. }
  19. this.req = new MockRequest()
  20. this.res = new MockResponse()
  21. this.Settings = {}
  22. this.TokenAccessHandler = {
  23. TOKEN_TYPES: {
  24. READ_ONLY: 'readOnly',
  25. READ_AND_WRITE: 'readAndWrite',
  26. },
  27. isReadAndWriteToken: sinon.stub().returns(true),
  28. isReadOnlyToken: sinon.stub().returns(true),
  29. tokenAccessEnabledForProject: sinon.stub().returns(true),
  30. checkTokenHashPrefix: sinon.stub(),
  31. makeTokenUrl: sinon.stub().returns('/'),
  32. grantSessionTokenAccess: sinon.stub(),
  33. promises: {
  34. addReadAndWriteUserToProject: sinon.stub().resolves(),
  35. addReadOnlyUserToProject: sinon.stub().resolves(),
  36. getProjectByToken: sinon.stub().resolves(this.project),
  37. getV1DocPublishedInfo: sinon.stub().resolves({ allow: true }),
  38. getV1DocInfo: sinon.stub(),
  39. },
  40. }
  41. this.SessionManager = {
  42. getLoggedInUserId: sinon.stub().returns(this.user._id),
  43. }
  44. this.AuthenticationController = {
  45. setRedirectInSession: sinon.stub(),
  46. }
  47. this.AuthorizationManager = {
  48. promises: {
  49. getPrivilegeLevelForProject: sinon
  50. .stub()
  51. .resolves(PrivilegeLevels.NONE),
  52. },
  53. }
  54. this.AuthorizationMiddleware = {}
  55. this.ProjectAuditLogHandler = {
  56. promises: {
  57. addEntry: sinon.stub().resolves(),
  58. },
  59. }
  60. this.TokenAccessController = SandboxedModule.require(MODULE_PATH, {
  61. requires: {
  62. '@overleaf/settings': this.Settings,
  63. './TokenAccessHandler': this.TokenAccessHandler,
  64. '../Authentication/AuthenticationController':
  65. this.AuthenticationController,
  66. '../Authentication/SessionManager': this.SessionManager,
  67. '../Authorization/AuthorizationManager': this.AuthorizationManager,
  68. '../Authorization/AuthorizationMiddleware':
  69. this.AuthorizationMiddleware,
  70. '../Project/ProjectAuditLogHandler': this.ProjectAuditLogHandler,
  71. '../Errors/Errors': (this.Errors = { NotFoundError: sinon.stub() }),
  72. },
  73. })
  74. })
  75. describe('grantTokenAccessReadAndWrite', function () {
  76. describe('normal case', function () {
  77. beforeEach(function (done) {
  78. this.req.params = { token: this.token }
  79. this.req.body = { confirmedByUser: true, tokenHashPrefix: '#prefix' }
  80. this.res.callback = done
  81. this.TokenAccessController.grantTokenAccessReadAndWrite(
  82. this.req,
  83. this.res,
  84. done
  85. )
  86. })
  87. it('grants read and write access', function () {
  88. expect(
  89. this.TokenAccessHandler.promises.addReadAndWriteUserToProject
  90. ).to.have.been.calledWith(this.user._id, this.project._id)
  91. })
  92. it('writes a project audit log', function () {
  93. expect(
  94. this.ProjectAuditLogHandler.promises.addEntry
  95. ).to.have.been.calledWith(
  96. this.project._id,
  97. 'join-via-token',
  98. this.user._id,
  99. this.req.ip,
  100. { privileges: 'readAndWrite' }
  101. )
  102. })
  103. it('checks token hash', function () {
  104. expect(
  105. this.TokenAccessHandler.checkTokenHashPrefix
  106. ).to.have.been.calledWith(
  107. this.token,
  108. '#prefix',
  109. 'readAndWrite',
  110. this.user._id,
  111. { projectId: this.project._id, action: 'continue' }
  112. )
  113. })
  114. })
  115. describe('when the access was already granted', function () {
  116. beforeEach(function (done) {
  117. this.project.tokenAccessReadAndWrite_refs.push(this.user._id)
  118. this.req.params = { token: this.token }
  119. this.req.body = { confirmedByUser: true }
  120. this.res.callback = done
  121. this.TokenAccessController.grantTokenAccessReadAndWrite(
  122. this.req,
  123. this.res,
  124. done
  125. )
  126. })
  127. it("doesn't write a project audit log", function () {
  128. expect(this.ProjectAuditLogHandler.promises.addEntry).to.not.have.been
  129. .called
  130. })
  131. it('checks token hash', function () {
  132. expect(
  133. this.TokenAccessHandler.checkTokenHashPrefix
  134. ).to.have.been.calledWith(
  135. this.token,
  136. undefined,
  137. 'readAndWrite',
  138. this.user._id,
  139. { projectId: this.project._id, action: 'continue' }
  140. )
  141. })
  142. })
  143. describe('hash prefix missing in request', function () {
  144. beforeEach(function (done) {
  145. this.req.params = { token: this.token }
  146. this.req.body = { confirmedByUser: true }
  147. this.res.callback = done
  148. this.TokenAccessController.grantTokenAccessReadAndWrite(
  149. this.req,
  150. this.res,
  151. done
  152. )
  153. })
  154. it('grants read and write access', function () {
  155. expect(
  156. this.TokenAccessHandler.promises.addReadAndWriteUserToProject
  157. ).to.have.been.calledWith(this.user._id, this.project._id)
  158. })
  159. it('checks the hash prefix', function () {
  160. expect(
  161. this.TokenAccessHandler.checkTokenHashPrefix
  162. ).to.have.been.calledWith(
  163. this.token,
  164. undefined,
  165. 'readAndWrite',
  166. this.user._id,
  167. { projectId: this.project._id, action: 'continue' }
  168. )
  169. })
  170. })
  171. describe('user is owner of project', function () {
  172. beforeEach(function (done) {
  173. this.AuthorizationManager.promises.getPrivilegeLevelForProject.returns(
  174. PrivilegeLevels.OWNER
  175. )
  176. this.req.params = { token: this.token }
  177. this.req.body = {}
  178. this.res.callback = done
  179. this.TokenAccessController.grantTokenAccessReadAndWrite(
  180. this.req,
  181. this.res,
  182. done
  183. )
  184. })
  185. it('checks token hash and includes log data', function () {
  186. expect(
  187. this.TokenAccessHandler.checkTokenHashPrefix
  188. ).to.have.been.calledWith(
  189. this.token,
  190. undefined,
  191. 'readAndWrite',
  192. this.user._id,
  193. {
  194. projectId: this.project._id,
  195. action: 'user already has higher or same privilege',
  196. }
  197. )
  198. })
  199. })
  200. describe('when user is not logged in', function () {
  201. beforeEach(function () {
  202. this.SessionManager.getLoggedInUserId.returns(null)
  203. this.req.params = { token: this.token }
  204. this.req.body = { tokenHashPrefix: '#prefix' }
  205. })
  206. describe('ANONYMOUS_READ_AND_WRITE_ENABLED is undefined', function () {
  207. beforeEach(function (done) {
  208. this.res.callback = done
  209. this.TokenAccessController.grantTokenAccessReadAndWrite(
  210. this.req,
  211. this.res,
  212. done
  213. )
  214. })
  215. it('redirects to restricted', function () {
  216. expect(this.res.json).to.have.been.calledWith({
  217. redirect: '/restricted',
  218. anonWriteAccessDenied: true,
  219. })
  220. })
  221. it('checks the hash prefix and includes log data', function () {
  222. expect(
  223. this.TokenAccessHandler.checkTokenHashPrefix
  224. ).to.have.been.calledWith(
  225. this.token,
  226. '#prefix',
  227. 'readAndWrite',
  228. null,
  229. {
  230. action: 'denied anonymous read-and-write token access',
  231. }
  232. )
  233. })
  234. it('saves redirect URL with URL fragment', function () {
  235. expect(
  236. this.AuthenticationController.setRedirectInSession.lastCall.args[1]
  237. ).to.equal('/#prefix')
  238. })
  239. })
  240. describe('ANONYMOUS_READ_AND_WRITE_ENABLED is true', function () {
  241. beforeEach(function (done) {
  242. this.TokenAccessHandler.ANONYMOUS_READ_AND_WRITE_ENABLED = true
  243. this.res.callback = done
  244. this.TokenAccessController.grantTokenAccessReadAndWrite(
  245. this.req,
  246. this.res,
  247. done
  248. )
  249. })
  250. it('redirects to project', function () {
  251. expect(this.res.json).to.have.been.calledWith({
  252. redirect: `/project/${this.project._id}`,
  253. grantAnonymousAccess: 'readAndWrite',
  254. })
  255. })
  256. it('checks the hash prefix and includes log data', function () {
  257. expect(
  258. this.TokenAccessHandler.checkTokenHashPrefix
  259. ).to.have.been.calledWith(
  260. this.token,
  261. '#prefix',
  262. 'readAndWrite',
  263. null,
  264. {
  265. projectId: this.project._id,
  266. action: 'granting read-write anonymous access',
  267. }
  268. )
  269. })
  270. })
  271. })
  272. describe('when Overleaf SaaS', function () {
  273. beforeEach(function () {
  274. this.Settings.overleaf = {}
  275. })
  276. describe('when token is for v1 project', function () {
  277. beforeEach(function (done) {
  278. this.TokenAccessHandler.promises.getProjectByToken.resolves(undefined)
  279. this.TokenAccessHandler.promises.getV1DocInfo.resolves({
  280. exists: true,
  281. has_owner: true,
  282. })
  283. this.req.params = { token: this.token }
  284. this.req.body = { tokenHashPrefix: '#prefix' }
  285. this.res.callback = done
  286. this.TokenAccessController.grantTokenAccessReadAndWrite(
  287. this.req,
  288. this.res,
  289. done
  290. )
  291. })
  292. it('returns v1 import data', function () {
  293. expect(this.res.json).to.have.been.calledWith({
  294. v1Import: {
  295. status: 'canDownloadZip',
  296. projectId: this.token,
  297. hasOwner: true,
  298. name: 'Untitled',
  299. brandInfo: undefined,
  300. },
  301. })
  302. })
  303. it('checks the hash prefix and includes log data', function () {
  304. expect(
  305. this.TokenAccessHandler.checkTokenHashPrefix
  306. ).to.have.been.calledWith(
  307. this.token,
  308. '#prefix',
  309. 'readAndWrite',
  310. this.user._id,
  311. {
  312. action: 'import v1',
  313. }
  314. )
  315. })
  316. })
  317. describe('when token is not for a v1 or v2 project', function () {
  318. beforeEach(function (done) {
  319. this.TokenAccessHandler.promises.getProjectByToken.resolves(undefined)
  320. this.TokenAccessHandler.promises.getV1DocInfo.resolves({
  321. exists: false,
  322. })
  323. this.req.params = { token: this.token }
  324. this.req.body = { tokenHashPrefix: '#prefix' }
  325. this.res.callback = done
  326. this.TokenAccessController.grantTokenAccessReadAndWrite(
  327. this.req,
  328. this.res,
  329. done
  330. )
  331. })
  332. it('returns 404', function () {
  333. expect(this.res.sendStatus).to.have.been.calledWith(404)
  334. })
  335. it('checks the hash prefix and includes log data', function () {
  336. expect(
  337. this.TokenAccessHandler.checkTokenHashPrefix
  338. ).to.have.been.calledWith(
  339. this.token,
  340. '#prefix',
  341. 'readAndWrite',
  342. this.user._id,
  343. {
  344. action: '404',
  345. }
  346. )
  347. })
  348. })
  349. })
  350. describe('not Overleaf SaaS', function () {
  351. beforeEach(function () {
  352. this.TokenAccessHandler.promises.getProjectByToken.resolves(undefined)
  353. this.req.params = { token: this.token }
  354. this.req.body = { tokenHashPrefix: '#prefix' }
  355. })
  356. it('passes Errors.NotFoundError to next when project not found and still checks token hash', function (done) {
  357. this.TokenAccessController.grantTokenAccessReadAndWrite(
  358. this.req,
  359. this.res,
  360. args => {
  361. expect(args).to.be.instanceof(this.Errors.NotFoundError)
  362. expect(
  363. this.TokenAccessHandler.checkTokenHashPrefix
  364. ).to.have.been.calledWith(
  365. this.token,
  366. '#prefix',
  367. 'readAndWrite',
  368. this.user._id,
  369. {
  370. action: '404',
  371. }
  372. )
  373. done()
  374. }
  375. )
  376. })
  377. })
  378. it('passes Errors.NotFoundError to next when token access is not enabled but still checks token hash', function (done) {
  379. this.TokenAccessHandler.tokenAccessEnabledForProject.returns(false)
  380. this.req.params = { token: this.token }
  381. this.req.body = { tokenHashPrefix: '#prefix' }
  382. this.TokenAccessController.grantTokenAccessReadAndWrite(
  383. this.req,
  384. this.res,
  385. args => {
  386. expect(args).to.be.instanceof(this.Errors.NotFoundError)
  387. expect(
  388. this.TokenAccessHandler.checkTokenHashPrefix
  389. ).to.have.been.calledWith(
  390. this.token,
  391. '#prefix',
  392. 'readAndWrite',
  393. this.user._id,
  394. {
  395. projectId: this.project._id,
  396. action: 'token access not enabled',
  397. }
  398. )
  399. done()
  400. }
  401. )
  402. })
  403. it('returns 400 when not using a read write token', function () {
  404. this.TokenAccessHandler.isReadAndWriteToken.returns(false)
  405. this.req.params = { token: this.token }
  406. this.req.body = { tokenHashPrefix: '#prefix' }
  407. this.TokenAccessController.grantTokenAccessReadAndWrite(
  408. this.req,
  409. this.res
  410. )
  411. expect(this.res.sendStatus).to.have.been.calledWith(400)
  412. })
  413. })
  414. describe('grantTokenAccessReadOnly', function () {
  415. describe('normal case', function () {
  416. beforeEach(function (done) {
  417. this.req.params = { token: this.token }
  418. this.req.body = { confirmedByUser: true, tokenHashPrefix: '#prefix' }
  419. this.res.callback = done
  420. this.TokenAccessController.grantTokenAccessReadOnly(
  421. this.req,
  422. this.res,
  423. done
  424. )
  425. })
  426. it('grants read-only access', function () {
  427. expect(
  428. this.TokenAccessHandler.promises.addReadOnlyUserToProject
  429. ).to.have.been.calledWith(this.user._id, this.project._id)
  430. })
  431. it('writes a project audit log', function () {
  432. expect(
  433. this.ProjectAuditLogHandler.promises.addEntry
  434. ).to.have.been.calledWith(
  435. this.project._id,
  436. 'join-via-token',
  437. this.user._id,
  438. this.req.ip,
  439. { privileges: 'readOnly' }
  440. )
  441. })
  442. it('checks if hash prefix matches', function () {
  443. expect(
  444. this.TokenAccessHandler.checkTokenHashPrefix
  445. ).to.have.been.calledWith(
  446. this.token,
  447. '#prefix',
  448. 'readOnly',
  449. this.user._id,
  450. { projectId: this.project._id, action: 'continue' }
  451. )
  452. })
  453. })
  454. describe('when the access was already granted', function () {
  455. beforeEach(function (done) {
  456. this.project.tokenAccessReadOnly_refs.push(this.user._id)
  457. this.req.params = { token: this.token }
  458. this.req.body = { confirmedByUser: true }
  459. this.res.callback = done
  460. this.TokenAccessController.grantTokenAccessReadOnly(
  461. this.req,
  462. this.res,
  463. done
  464. )
  465. })
  466. it("doesn't write a project audit log", function () {
  467. expect(this.ProjectAuditLogHandler.promises.addEntry).to.not.have.been
  468. .called
  469. })
  470. it('still checks if hash prefix matches', function () {
  471. expect(
  472. this.TokenAccessHandler.checkTokenHashPrefix
  473. ).to.have.been.calledWith(
  474. this.token,
  475. undefined,
  476. 'readOnly',
  477. this.user._id,
  478. { projectId: this.project._id, action: 'continue' }
  479. )
  480. })
  481. })
  482. it('returns 400 when not using a read only token', function () {
  483. this.TokenAccessHandler.isReadOnlyToken.returns(false)
  484. this.req.params = { token: this.token }
  485. this.req.body = { tokenHashPrefix: '#prefix' }
  486. this.TokenAccessController.grantTokenAccessReadOnly(this.req, this.res)
  487. expect(this.res.sendStatus).to.have.been.calledWith(400)
  488. })
  489. describe('anonymous users', function () {
  490. beforeEach(function (done) {
  491. this.req.params = { token: this.token }
  492. this.SessionManager.getLoggedInUserId.returns(null)
  493. this.res.callback = done
  494. this.TokenAccessController.grantTokenAccessReadOnly(
  495. this.req,
  496. this.res,
  497. done
  498. )
  499. })
  500. it('allows anonymous users and checks the token hash', function () {
  501. expect(this.res.json).to.have.been.calledWith({
  502. redirect: `/project/${this.project._id}`,
  503. grantAnonymousAccess: 'readOnly',
  504. })
  505. expect(
  506. this.TokenAccessHandler.checkTokenHashPrefix
  507. ).to.have.been.calledWith(this.token, undefined, 'readOnly', null, {
  508. projectId: this.project._id,
  509. action: 'granting read-only anonymous access',
  510. })
  511. })
  512. })
  513. describe('user is owner of project', function () {
  514. beforeEach(function (done) {
  515. this.AuthorizationManager.promises.getPrivilegeLevelForProject.returns(
  516. PrivilegeLevels.OWNER
  517. )
  518. this.req.params = { token: this.token }
  519. this.req.body = {}
  520. this.res.callback = done
  521. this.TokenAccessController.grantTokenAccessReadOnly(
  522. this.req,
  523. this.res,
  524. done
  525. )
  526. })
  527. it('checks token hash and includes log data', function () {
  528. expect(
  529. this.TokenAccessHandler.checkTokenHashPrefix
  530. ).to.have.been.calledWith(
  531. this.token,
  532. undefined,
  533. 'readOnly',
  534. this.user._id,
  535. {
  536. projectId: this.project._id,
  537. action: 'user already has higher or same privilege',
  538. }
  539. )
  540. })
  541. })
  542. it('passes Errors.NotFoundError to next when token access is not enabled but still checks token hash', function (done) {
  543. this.TokenAccessHandler.tokenAccessEnabledForProject.returns(false)
  544. this.req.params = { token: this.token }
  545. this.req.body = { tokenHashPrefix: '#prefix' }
  546. this.TokenAccessController.grantTokenAccessReadOnly(
  547. this.req,
  548. this.res,
  549. args => {
  550. expect(args).to.be.instanceof(this.Errors.NotFoundError)
  551. expect(
  552. this.TokenAccessHandler.checkTokenHashPrefix
  553. ).to.have.been.calledWith(
  554. this.token,
  555. '#prefix',
  556. 'readOnly',
  557. this.user._id,
  558. {
  559. projectId: this.project._id,
  560. action: 'token access not enabled',
  561. }
  562. )
  563. done()
  564. }
  565. )
  566. })
  567. })
  568. })