TokenAccessControllerTests.js 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952
  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. name: 'test',
  17. tokenAccessReadAndWrite_refs: [],
  18. tokenAccessReadOnly_refs: [],
  19. }
  20. this.req = new MockRequest()
  21. this.res = new MockResponse()
  22. this.next = sinon.stub().returns()
  23. this.Settings = {}
  24. this.TokenAccessHandler = {
  25. TOKEN_TYPES: {
  26. READ_ONLY: 'readOnly',
  27. READ_AND_WRITE: 'readAndWrite',
  28. },
  29. isReadAndWriteToken: sinon.stub().returns(true),
  30. isReadOnlyToken: sinon.stub().returns(true),
  31. tokenAccessEnabledForProject: sinon.stub().returns(true),
  32. checkTokenHashPrefix: sinon.stub(),
  33. makeTokenUrl: sinon.stub().returns('/'),
  34. grantSessionTokenAccess: sinon.stub(),
  35. promises: {
  36. addReadAndWriteUserToProject: sinon.stub().resolves(),
  37. addReadOnlyUserToProject: sinon.stub().resolves(),
  38. getProjectByToken: sinon.stub().resolves(this.project),
  39. getV1DocPublishedInfo: sinon.stub().resolves({ allow: true }),
  40. getV1DocInfo: sinon.stub(),
  41. removeReadAndWriteUserFromProject: sinon.stub().resolves(),
  42. moveReadAndWriteUserToReadOnly: sinon.stub().resolves(),
  43. },
  44. }
  45. this.SessionManager = {
  46. getLoggedInUserId: sinon.stub().returns(this.user._id),
  47. }
  48. this.AuthenticationController = {
  49. setRedirectInSession: sinon.stub(),
  50. }
  51. this.AuthorizationManager = {
  52. promises: {
  53. getPrivilegeLevelForProject: sinon
  54. .stub()
  55. .resolves(PrivilegeLevels.NONE),
  56. },
  57. }
  58. this.AuthorizationMiddleware = {}
  59. this.ProjectAuditLogHandler = {
  60. promises: {
  61. addEntry: sinon.stub().resolves(),
  62. },
  63. }
  64. this.SplitTestHandler = {
  65. promises: {
  66. getAssignment: sinon.stub().resolves({ variant: 'default' }),
  67. getAssignmentForUser: sinon.stub().resolves({ variant: 'default' }),
  68. },
  69. }
  70. this.CollaboratorsHandler = {
  71. promises: {
  72. addUserIdToProject: sinon.stub().resolves(),
  73. setCollaboratorPrivilegeLevel: sinon.stub().resolves(),
  74. },
  75. }
  76. this.CollaboratorsGetter = {
  77. promises: {
  78. userIsReadWriteTokenMember: sinon.stub().resolves(),
  79. isUserInvitedReadWriteMemberOfProject: sinon.stub().resolves(),
  80. isUserInvitedMemberOfProject: sinon.stub().resolves(),
  81. },
  82. }
  83. this.EditorRealTimeController = { emitToRoom: sinon.stub() }
  84. this.ProjectGetter = {
  85. promises: {
  86. getProject: sinon.stub().resolves(this.project),
  87. },
  88. }
  89. this.AnalyticsManager = {
  90. recordEventForSession: sinon.stub(),
  91. }
  92. this.TokenAccessController = SandboxedModule.require(MODULE_PATH, {
  93. requires: {
  94. '@overleaf/settings': this.Settings,
  95. './TokenAccessHandler': this.TokenAccessHandler,
  96. '../Authentication/AuthenticationController':
  97. this.AuthenticationController,
  98. '../Authentication/SessionManager': this.SessionManager,
  99. '../Authorization/AuthorizationManager': this.AuthorizationManager,
  100. '../Authorization/AuthorizationMiddleware':
  101. this.AuthorizationMiddleware,
  102. '../Project/ProjectAuditLogHandler': this.ProjectAuditLogHandler,
  103. '../SplitTests/SplitTestHandler': this.SplitTestHandler,
  104. '../Errors/Errors': (this.Errors = { NotFoundError: sinon.stub() }),
  105. '../Collaborators/CollaboratorsHandler': this.CollaboratorsHandler,
  106. '../Collaborators/CollaboratorsGetter': this.CollaboratorsGetter,
  107. '../Editor/EditorRealTimeController': this.EditorRealTimeController,
  108. '../Project/ProjectGetter': this.ProjectGetter,
  109. '../Helpers/AsyncFormHelper': (this.AsyncFormHelper = {
  110. redirect: sinon.stub(),
  111. }),
  112. '../Analytics/AnalyticsManager': this.AnalyticsManager,
  113. },
  114. })
  115. })
  116. describe('grantTokenAccessReadAndWrite', function () {
  117. describe('normal case', function () {
  118. beforeEach(function (done) {
  119. this.req.params = { token: this.token }
  120. this.req.body = { confirmedByUser: true, tokenHashPrefix: '#prefix' }
  121. this.res.callback = done
  122. this.TokenAccessController.grantTokenAccessReadAndWrite(
  123. this.req,
  124. this.res,
  125. done
  126. )
  127. })
  128. it('grants read and write access', function () {
  129. expect(
  130. this.TokenAccessHandler.promises.addReadAndWriteUserToProject
  131. ).to.have.been.calledWith(this.user._id, this.project._id)
  132. })
  133. it('writes a project audit log', function () {
  134. expect(
  135. this.ProjectAuditLogHandler.promises.addEntry
  136. ).to.have.been.calledWith(
  137. this.project._id,
  138. 'join-via-token',
  139. this.user._id,
  140. this.req.ip,
  141. { privileges: 'readAndWrite' }
  142. )
  143. })
  144. it('checks token hash', function () {
  145. expect(
  146. this.TokenAccessHandler.checkTokenHashPrefix
  147. ).to.have.been.calledWith(
  148. this.token,
  149. '#prefix',
  150. 'readAndWrite',
  151. this.user._id,
  152. { projectId: this.project._id, action: 'continue' }
  153. )
  154. })
  155. })
  156. describe('when project owner in link-sharing-warning split test', function () {
  157. beforeEach(function () {
  158. this.SplitTestHandler.promises.getAssignmentForUser.resolves({
  159. variant: 'active',
  160. })
  161. })
  162. it('tells the ui to show the link-sharing-warning variant', async function () {
  163. this.req.params = { token: this.token }
  164. this.req.body = { tokenHashPrefix: '#prefix' }
  165. await this.TokenAccessController.grantTokenAccessReadAndWrite(
  166. this.req,
  167. {
  168. json: content => {
  169. expect(content).to.deep.equal({
  170. requireAccept: {
  171. linkSharingChanges: true,
  172. projectName: this.project.name,
  173. },
  174. })
  175. },
  176. }
  177. )
  178. })
  179. describe('normal case', function () {
  180. beforeEach(function (done) {
  181. this.req.params = { token: this.token }
  182. this.req.body = { confirmedByUser: true, tokenHashPrefix: '#prefix' }
  183. this.res.callback = done
  184. this.TokenAccessController.grantTokenAccessReadAndWrite(
  185. this.req,
  186. this.res,
  187. done
  188. )
  189. })
  190. it('adds the user as a read and write invited member', function () {
  191. expect(
  192. this.CollaboratorsHandler.promises.addUserIdToProject
  193. ).to.have.been.calledWith(
  194. this.project._id,
  195. undefined,
  196. this.user._id,
  197. PrivilegeLevels.READ_AND_WRITE
  198. )
  199. })
  200. it('writes a project audit log', function () {
  201. expect(
  202. this.ProjectAuditLogHandler.promises.addEntry
  203. ).to.have.been.calledWith(
  204. this.project._id,
  205. 'accept-via-link-sharing',
  206. this.user._id,
  207. this.req.ip,
  208. { privileges: 'readAndWrite' }
  209. )
  210. })
  211. it('emits a project membership changed event', function () {
  212. expect(
  213. this.EditorRealTimeController.emitToRoom
  214. ).to.have.been.calledWith(
  215. this.project._id,
  216. 'project:membership:changed',
  217. { members: true }
  218. )
  219. })
  220. it('checks token hash', function () {
  221. expect(
  222. this.TokenAccessHandler.checkTokenHashPrefix
  223. ).to.have.been.calledWith(
  224. this.token,
  225. '#prefix',
  226. 'readAndWrite',
  227. this.user._id,
  228. { projectId: this.project._id, action: 'continue' }
  229. )
  230. })
  231. })
  232. })
  233. describe('when the access was already granted', function () {
  234. beforeEach(function (done) {
  235. this.project.tokenAccessReadAndWrite_refs.push(this.user._id)
  236. this.req.params = { token: this.token }
  237. this.req.body = { confirmedByUser: true }
  238. this.res.callback = done
  239. this.TokenAccessController.grantTokenAccessReadAndWrite(
  240. this.req,
  241. this.res,
  242. done
  243. )
  244. })
  245. it("doesn't write a project audit log", function () {
  246. expect(this.ProjectAuditLogHandler.promises.addEntry).to.not.have.been
  247. .called
  248. })
  249. it('checks token hash', function () {
  250. expect(
  251. this.TokenAccessHandler.checkTokenHashPrefix
  252. ).to.have.been.calledWith(
  253. this.token,
  254. undefined,
  255. 'readAndWrite',
  256. this.user._id,
  257. { projectId: this.project._id, action: 'continue' }
  258. )
  259. })
  260. })
  261. describe('hash prefix missing in request', function () {
  262. beforeEach(function (done) {
  263. this.req.params = { token: this.token }
  264. this.req.body = { confirmedByUser: true }
  265. this.res.callback = done
  266. this.TokenAccessController.grantTokenAccessReadAndWrite(
  267. this.req,
  268. this.res,
  269. done
  270. )
  271. })
  272. it('grants read and write access', function () {
  273. expect(
  274. this.TokenAccessHandler.promises.addReadAndWriteUserToProject
  275. ).to.have.been.calledWith(this.user._id, this.project._id)
  276. })
  277. it('checks the hash prefix', function () {
  278. expect(
  279. this.TokenAccessHandler.checkTokenHashPrefix
  280. ).to.have.been.calledWith(
  281. this.token,
  282. undefined,
  283. 'readAndWrite',
  284. this.user._id,
  285. { projectId: this.project._id, action: 'continue' }
  286. )
  287. })
  288. })
  289. describe('user is owner of project', function () {
  290. beforeEach(function (done) {
  291. this.AuthorizationManager.promises.getPrivilegeLevelForProject.returns(
  292. PrivilegeLevels.OWNER
  293. )
  294. this.req.params = { token: this.token }
  295. this.req.body = {}
  296. this.res.callback = done
  297. this.TokenAccessController.grantTokenAccessReadAndWrite(
  298. this.req,
  299. this.res,
  300. done
  301. )
  302. })
  303. it('checks token hash and includes log data', function () {
  304. expect(
  305. this.TokenAccessHandler.checkTokenHashPrefix
  306. ).to.have.been.calledWith(
  307. this.token,
  308. undefined,
  309. 'readAndWrite',
  310. this.user._id,
  311. {
  312. projectId: this.project._id,
  313. action: 'user already has higher or same privilege',
  314. }
  315. )
  316. })
  317. })
  318. describe('when user is not logged in', function () {
  319. beforeEach(function () {
  320. this.SessionManager.getLoggedInUserId.returns(null)
  321. this.req.params = { token: this.token }
  322. this.req.body = { tokenHashPrefix: '#prefix' }
  323. })
  324. describe('ANONYMOUS_READ_AND_WRITE_ENABLED is undefined', function () {
  325. beforeEach(function (done) {
  326. this.res.callback = done
  327. this.TokenAccessController.grantTokenAccessReadAndWrite(
  328. this.req,
  329. this.res,
  330. done
  331. )
  332. })
  333. it('redirects to restricted', function () {
  334. expect(this.res.json).to.have.been.calledWith({
  335. redirect: '/restricted',
  336. anonWriteAccessDenied: true,
  337. })
  338. })
  339. it('checks the hash prefix and includes log data', function () {
  340. expect(
  341. this.TokenAccessHandler.checkTokenHashPrefix
  342. ).to.have.been.calledWith(
  343. this.token,
  344. '#prefix',
  345. 'readAndWrite',
  346. null,
  347. {
  348. action: 'denied anonymous read-and-write token access',
  349. }
  350. )
  351. })
  352. it('saves redirect URL with URL fragment', function () {
  353. expect(
  354. this.AuthenticationController.setRedirectInSession.lastCall.args[1]
  355. ).to.equal('/#prefix')
  356. })
  357. })
  358. describe('ANONYMOUS_READ_AND_WRITE_ENABLED is true', function () {
  359. beforeEach(function (done) {
  360. this.TokenAccessHandler.ANONYMOUS_READ_AND_WRITE_ENABLED = true
  361. this.res.callback = done
  362. this.TokenAccessController.grantTokenAccessReadAndWrite(
  363. this.req,
  364. this.res,
  365. done
  366. )
  367. })
  368. it('redirects to project', function () {
  369. expect(this.res.json).to.have.been.calledWith({
  370. redirect: `/project/${this.project._id}`,
  371. grantAnonymousAccess: 'readAndWrite',
  372. })
  373. })
  374. it('checks the hash prefix and includes log data', function () {
  375. expect(
  376. this.TokenAccessHandler.checkTokenHashPrefix
  377. ).to.have.been.calledWith(
  378. this.token,
  379. '#prefix',
  380. 'readAndWrite',
  381. null,
  382. {
  383. projectId: this.project._id,
  384. action: 'granting read-write anonymous access',
  385. }
  386. )
  387. })
  388. })
  389. })
  390. describe('when Overleaf SaaS', function () {
  391. beforeEach(function () {
  392. this.Settings.overleaf = {}
  393. })
  394. describe('when token is for v1 project', function () {
  395. beforeEach(function (done) {
  396. this.TokenAccessHandler.promises.getProjectByToken.resolves(undefined)
  397. this.TokenAccessHandler.promises.getV1DocInfo.resolves({
  398. exists: true,
  399. has_owner: true,
  400. })
  401. this.req.params = { token: this.token }
  402. this.req.body = { tokenHashPrefix: '#prefix' }
  403. this.res.callback = done
  404. this.TokenAccessController.grantTokenAccessReadAndWrite(
  405. this.req,
  406. this.res,
  407. done
  408. )
  409. })
  410. it('returns v1 import data', function () {
  411. expect(this.res.json).to.have.been.calledWith({
  412. v1Import: {
  413. status: 'canDownloadZip',
  414. projectId: this.token,
  415. hasOwner: true,
  416. name: 'Untitled',
  417. brandInfo: undefined,
  418. },
  419. })
  420. })
  421. it('checks the hash prefix and includes log data', function () {
  422. expect(
  423. this.TokenAccessHandler.checkTokenHashPrefix
  424. ).to.have.been.calledWith(
  425. this.token,
  426. '#prefix',
  427. 'readAndWrite',
  428. this.user._id,
  429. {
  430. action: 'import v1',
  431. }
  432. )
  433. })
  434. })
  435. describe('when token is not for a v1 or v2 project', function () {
  436. beforeEach(function (done) {
  437. this.TokenAccessHandler.promises.getProjectByToken.resolves(undefined)
  438. this.TokenAccessHandler.promises.getV1DocInfo.resolves({
  439. exists: false,
  440. })
  441. this.req.params = { token: this.token }
  442. this.req.body = { tokenHashPrefix: '#prefix' }
  443. this.res.callback = done
  444. this.TokenAccessController.grantTokenAccessReadAndWrite(
  445. this.req,
  446. this.res,
  447. done
  448. )
  449. })
  450. it('returns 404', function () {
  451. expect(this.res.sendStatus).to.have.been.calledWith(404)
  452. })
  453. it('checks the hash prefix and includes log data', function () {
  454. expect(
  455. this.TokenAccessHandler.checkTokenHashPrefix
  456. ).to.have.been.calledWith(
  457. this.token,
  458. '#prefix',
  459. 'readAndWrite',
  460. this.user._id,
  461. {
  462. action: '404',
  463. }
  464. )
  465. })
  466. })
  467. })
  468. describe('not Overleaf SaaS', function () {
  469. beforeEach(function () {
  470. this.TokenAccessHandler.promises.getProjectByToken.resolves(undefined)
  471. this.req.params = { token: this.token }
  472. this.req.body = { tokenHashPrefix: '#prefix' }
  473. })
  474. it('passes Errors.NotFoundError to next when project not found and still checks token hash', function (done) {
  475. this.TokenAccessController.grantTokenAccessReadAndWrite(
  476. this.req,
  477. this.res,
  478. args => {
  479. expect(args).to.be.instanceof(this.Errors.NotFoundError)
  480. expect(
  481. this.TokenAccessHandler.checkTokenHashPrefix
  482. ).to.have.been.calledWith(
  483. this.token,
  484. '#prefix',
  485. 'readAndWrite',
  486. this.user._id,
  487. {
  488. action: '404',
  489. }
  490. )
  491. done()
  492. }
  493. )
  494. })
  495. })
  496. it('passes Errors.NotFoundError to next when token access is not enabled but still checks token hash', function (done) {
  497. this.TokenAccessHandler.tokenAccessEnabledForProject.returns(false)
  498. this.req.params = { token: this.token }
  499. this.req.body = { tokenHashPrefix: '#prefix' }
  500. this.TokenAccessController.grantTokenAccessReadAndWrite(
  501. this.req,
  502. this.res,
  503. args => {
  504. expect(args).to.be.instanceof(this.Errors.NotFoundError)
  505. expect(
  506. this.TokenAccessHandler.checkTokenHashPrefix
  507. ).to.have.been.calledWith(
  508. this.token,
  509. '#prefix',
  510. 'readAndWrite',
  511. this.user._id,
  512. {
  513. projectId: this.project._id,
  514. action: 'token access not enabled',
  515. }
  516. )
  517. done()
  518. }
  519. )
  520. })
  521. it('returns 400 when not using a read write token', function () {
  522. this.TokenAccessHandler.isReadAndWriteToken.returns(false)
  523. this.req.params = { token: this.token }
  524. this.req.body = { tokenHashPrefix: '#prefix' }
  525. this.TokenAccessController.grantTokenAccessReadAndWrite(
  526. this.req,
  527. this.res
  528. )
  529. expect(this.res.sendStatus).to.have.been.calledWith(400)
  530. })
  531. })
  532. describe('grantTokenAccessReadOnly', function () {
  533. describe('normal case', function () {
  534. beforeEach(function (done) {
  535. this.req.params = { token: this.token }
  536. this.req.body = { confirmedByUser: true, tokenHashPrefix: '#prefix' }
  537. this.res.callback = done
  538. this.TokenAccessController.grantTokenAccessReadOnly(
  539. this.req,
  540. this.res,
  541. done
  542. )
  543. })
  544. it('grants read-only access', function () {
  545. expect(
  546. this.TokenAccessHandler.promises.addReadOnlyUserToProject
  547. ).to.have.been.calledWith(this.user._id, this.project._id)
  548. })
  549. it('writes a project audit log', function () {
  550. expect(
  551. this.ProjectAuditLogHandler.promises.addEntry
  552. ).to.have.been.calledWith(
  553. this.project._id,
  554. 'join-via-token',
  555. this.user._id,
  556. this.req.ip,
  557. { privileges: 'readOnly' }
  558. )
  559. })
  560. it('checks if hash prefix matches', function () {
  561. expect(
  562. this.TokenAccessHandler.checkTokenHashPrefix
  563. ).to.have.been.calledWith(
  564. this.token,
  565. '#prefix',
  566. 'readOnly',
  567. this.user._id,
  568. { projectId: this.project._id, action: 'continue' }
  569. )
  570. })
  571. })
  572. describe('when the access was already granted', function () {
  573. beforeEach(function (done) {
  574. this.project.tokenAccessReadOnly_refs.push(this.user._id)
  575. this.req.params = { token: this.token }
  576. this.req.body = { confirmedByUser: true }
  577. this.res.callback = done
  578. this.TokenAccessController.grantTokenAccessReadOnly(
  579. this.req,
  580. this.res,
  581. done
  582. )
  583. })
  584. it("doesn't write a project audit log", function () {
  585. expect(this.ProjectAuditLogHandler.promises.addEntry).to.not.have.been
  586. .called
  587. })
  588. it('still checks if hash prefix matches', function () {
  589. expect(
  590. this.TokenAccessHandler.checkTokenHashPrefix
  591. ).to.have.been.calledWith(
  592. this.token,
  593. undefined,
  594. 'readOnly',
  595. this.user._id,
  596. { projectId: this.project._id, action: 'continue' }
  597. )
  598. })
  599. })
  600. it('returns 400 when not using a read only token', function () {
  601. this.TokenAccessHandler.isReadOnlyToken.returns(false)
  602. this.req.params = { token: this.token }
  603. this.req.body = { tokenHashPrefix: '#prefix' }
  604. this.TokenAccessController.grantTokenAccessReadOnly(this.req, this.res)
  605. expect(this.res.sendStatus).to.have.been.calledWith(400)
  606. })
  607. describe('anonymous users', function () {
  608. beforeEach(function (done) {
  609. this.req.params = { token: this.token }
  610. this.SessionManager.getLoggedInUserId.returns(null)
  611. this.res.callback = done
  612. this.TokenAccessController.grantTokenAccessReadOnly(
  613. this.req,
  614. this.res,
  615. done
  616. )
  617. })
  618. it('allows anonymous users and checks the token hash', function () {
  619. expect(this.res.json).to.have.been.calledWith({
  620. redirect: `/project/${this.project._id}`,
  621. grantAnonymousAccess: 'readOnly',
  622. })
  623. expect(
  624. this.TokenAccessHandler.checkTokenHashPrefix
  625. ).to.have.been.calledWith(this.token, undefined, 'readOnly', null, {
  626. projectId: this.project._id,
  627. action: 'granting read-only anonymous access',
  628. })
  629. })
  630. })
  631. describe('user is owner of project', function () {
  632. beforeEach(function (done) {
  633. this.AuthorizationManager.promises.getPrivilegeLevelForProject.returns(
  634. PrivilegeLevels.OWNER
  635. )
  636. this.req.params = { token: this.token }
  637. this.req.body = {}
  638. this.res.callback = done
  639. this.TokenAccessController.grantTokenAccessReadOnly(
  640. this.req,
  641. this.res,
  642. done
  643. )
  644. })
  645. it('checks token hash and includes log data', function () {
  646. expect(
  647. this.TokenAccessHandler.checkTokenHashPrefix
  648. ).to.have.been.calledWith(
  649. this.token,
  650. undefined,
  651. 'readOnly',
  652. this.user._id,
  653. {
  654. projectId: this.project._id,
  655. action: 'user already has higher or same privilege',
  656. }
  657. )
  658. })
  659. })
  660. it('passes Errors.NotFoundError to next when token access is not enabled but still checks token hash', function (done) {
  661. this.TokenAccessHandler.tokenAccessEnabledForProject.returns(false)
  662. this.req.params = { token: this.token }
  663. this.req.body = { tokenHashPrefix: '#prefix' }
  664. this.TokenAccessController.grantTokenAccessReadOnly(
  665. this.req,
  666. this.res,
  667. args => {
  668. expect(args).to.be.instanceof(this.Errors.NotFoundError)
  669. expect(
  670. this.TokenAccessHandler.checkTokenHashPrefix
  671. ).to.have.been.calledWith(
  672. this.token,
  673. '#prefix',
  674. 'readOnly',
  675. this.user._id,
  676. {
  677. projectId: this.project._id,
  678. action: 'token access not enabled',
  679. }
  680. )
  681. done()
  682. }
  683. )
  684. })
  685. })
  686. describe('ensureUserCanUseSharingUpdatesConsentPage', function () {
  687. beforeEach(function () {
  688. this.req.params = { Project_id: this.project._id }
  689. })
  690. describe('when not in link sharing changes test', function () {
  691. beforeEach(function (done) {
  692. this.AsyncFormHelper.redirect = sinon.stub().callsFake(() => done())
  693. this.TokenAccessController.ensureUserCanUseSharingUpdatesConsentPage(
  694. this.req,
  695. this.res,
  696. done
  697. )
  698. })
  699. it('redirects to the project/editor', function () {
  700. expect(this.AsyncFormHelper.redirect).to.have.been.calledWith(
  701. this.req,
  702. this.res,
  703. `/project/${this.project._id}`
  704. )
  705. })
  706. })
  707. describe('when link sharing changes test active', function () {
  708. beforeEach(function () {
  709. this.SplitTestHandler.promises.getAssignmentForUser.resolves({
  710. variant: 'active',
  711. })
  712. })
  713. describe('when user is not an invited editor and is a read write token member', function () {
  714. beforeEach(function (done) {
  715. this.CollaboratorsGetter.promises.isUserInvitedReadWriteMemberOfProject.resolves(
  716. false
  717. )
  718. this.CollaboratorsGetter.promises.userIsReadWriteTokenMember.resolves(
  719. true
  720. )
  721. this.next.callsFake(() => done())
  722. this.TokenAccessController.ensureUserCanUseSharingUpdatesConsentPage(
  723. this.req,
  724. this.res,
  725. this.next
  726. )
  727. })
  728. it('calls next', function () {
  729. expect(
  730. this.CollaboratorsGetter.promises
  731. .isUserInvitedReadWriteMemberOfProject
  732. ).to.have.been.calledWith(this.user._id, this.project._id)
  733. expect(
  734. this.CollaboratorsGetter.promises.userIsReadWriteTokenMember
  735. ).to.have.been.calledWith(this.user._id, this.project._id)
  736. expect(this.next).to.have.been.calledOnce
  737. expect(this.next.firstCall.args[0]).to.not.exist
  738. })
  739. })
  740. describe('when user is already an invited editor', function () {
  741. beforeEach(function (done) {
  742. this.CollaboratorsGetter.promises.isUserInvitedReadWriteMemberOfProject.resolves(
  743. true
  744. )
  745. this.AsyncFormHelper.redirect = sinon.stub().callsFake(() => done())
  746. this.TokenAccessController.ensureUserCanUseSharingUpdatesConsentPage(
  747. this.req,
  748. this.res,
  749. done
  750. )
  751. })
  752. it('redirects to the project/editor', function () {
  753. expect(this.AsyncFormHelper.redirect).to.have.been.calledWith(
  754. this.req,
  755. this.res,
  756. `/project/${this.project._id}`
  757. )
  758. })
  759. })
  760. describe('when user not a read write token member', function () {
  761. beforeEach(function (done) {
  762. this.CollaboratorsGetter.promises.userIsReadWriteTokenMember.resolves(
  763. false
  764. )
  765. this.AsyncFormHelper.redirect = sinon.stub().callsFake(() => done())
  766. this.TokenAccessController.ensureUserCanUseSharingUpdatesConsentPage(
  767. this.req,
  768. this.res,
  769. done
  770. )
  771. })
  772. it('redirects to the project/editor', function () {
  773. expect(this.AsyncFormHelper.redirect).to.have.been.calledWith(
  774. this.req,
  775. this.res,
  776. `/project/${this.project._id}`
  777. )
  778. })
  779. })
  780. })
  781. })
  782. describe('moveReadWriteToCollaborators', function () {
  783. beforeEach(function () {
  784. this.req.params = { Project_id: this.project._id }
  785. })
  786. describe('read only invited viewer gaining edit access via link sharing', function () {
  787. beforeEach(function (done) {
  788. this.CollaboratorsGetter.promises.isUserInvitedMemberOfProject.resolves(
  789. true
  790. )
  791. this.res.callback = done
  792. this.TokenAccessController.moveReadWriteToCollaborators(
  793. this.req,
  794. this.res,
  795. done
  796. )
  797. })
  798. it('sets the privilege level to read and write for the invited viewer', function () {
  799. expect(
  800. this.CollaboratorsHandler.promises.setCollaboratorPrivilegeLevel
  801. ).to.have.been.calledWith(
  802. this.project._id,
  803. this.user._id,
  804. PrivilegeLevels.READ_AND_WRITE
  805. )
  806. expect(this.res.sendStatus).to.have.been.calledWith(204)
  807. })
  808. })
  809. describe('previously joined token access user moving to named collaborator', function () {
  810. beforeEach(function (done) {
  811. this.CollaboratorsGetter.promises.isUserInvitedMemberOfProject.resolves(
  812. false
  813. )
  814. this.res.callback = done
  815. this.TokenAccessController.moveReadWriteToCollaborators(
  816. this.req,
  817. this.res,
  818. done
  819. )
  820. })
  821. it('sets the privilege level to read and write for the invited viewer', function () {
  822. expect(
  823. this.TokenAccessHandler.promises.removeReadAndWriteUserFromProject
  824. ).to.have.been.calledWith(this.user._id, this.project._id)
  825. expect(
  826. this.CollaboratorsHandler.promises.addUserIdToProject
  827. ).to.have.been.calledWith(
  828. this.project._id,
  829. undefined,
  830. this.user._id,
  831. PrivilegeLevels.READ_AND_WRITE
  832. )
  833. expect(this.res.sendStatus).to.have.been.calledWith(204)
  834. })
  835. })
  836. })
  837. describe('moveReadWriteToReadOnly', function () {
  838. beforeEach(function () {
  839. this.req.params = { Project_id: this.project._id }
  840. })
  841. describe('previously joined token access user moving to anonymous viewer', function () {
  842. beforeEach(function (done) {
  843. this.res.callback = done
  844. this.TokenAccessController.moveReadWriteToReadOnly(
  845. this.req,
  846. this.res,
  847. done
  848. )
  849. })
  850. it('removes them from read write token access refs and adds them to read only token access refs', function () {
  851. expect(
  852. this.TokenAccessHandler.promises.moveReadAndWriteUserToReadOnly
  853. ).to.have.been.calledWith(this.user._id, this.project._id)
  854. expect(this.res.sendStatus).to.have.been.calledWith(204)
  855. })
  856. it('writes a project audit log', function () {
  857. expect(
  858. this.ProjectAuditLogHandler.promises.addEntry
  859. ).to.have.been.calledWith(
  860. this.project._id,
  861. 'readonly-via-sharing-updates',
  862. this.user._id,
  863. this.req.ip
  864. )
  865. })
  866. })
  867. })
  868. })