TokenAccessControllerTests.js 41 KB

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