TokenAccessControllerTests.js 32 KB

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