TokenAccessControllerTests.js 32 KB

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