TokenAccessHandlerTests.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685
  1. /* eslint-disable
  2. node/handle-callback-err,
  3. max-len,
  4. no-return-assign,
  5. no-unused-vars,
  6. */
  7. // TODO: This file was created by bulk-decaffeinate.
  8. // Fix any style issues and re-enable lint.
  9. /*
  10. * decaffeinate suggestions:
  11. * DS102: Remove unnecessary code created because of implicit returns
  12. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  13. */
  14. const SandboxedModule = require('sandboxed-module')
  15. const path = require('path')
  16. const sinon = require('sinon')
  17. const modulePath = path.join(
  18. __dirname,
  19. '../../../../app/src/Features/TokenAccess/TokenAccessHandler'
  20. )
  21. const { expect } = require('chai')
  22. const { ObjectId } = require('mongodb')
  23. describe('TokenAccessHandler', function () {
  24. beforeEach(function () {
  25. this.token = 'abcdefabcdef'
  26. this.projectId = ObjectId()
  27. this.project = {
  28. _id: this.projectId,
  29. publicAccesLevel: 'tokenBased',
  30. owner_ref: ObjectId(),
  31. }
  32. this.userId = ObjectId()
  33. this.req = {}
  34. return (this.TokenAccessHandler = SandboxedModule.require(modulePath, {
  35. requires: {
  36. mongodb: { ObjectId },
  37. '../../models/Project': { Project: (this.Project = {}) },
  38. '@overleaf/settings': (this.settings = {}),
  39. '../V1/V1Api': (this.V1Api = {
  40. request: sinon.stub(),
  41. }),
  42. crypto: (this.Crypto = require('crypto')),
  43. '../Analytics/AnalyticsManager': (this.Analytics = {
  44. recordEventForUser: sinon.stub(),
  45. }),
  46. },
  47. }))
  48. })
  49. describe('getTokenType', function () {
  50. it('should determine tokens correctly', function () {
  51. const specs = {
  52. abcdefabcdef: 'readOnly',
  53. aaaaaabbbbbb: 'readOnly',
  54. '54325aaaaaa': 'readAndWrite',
  55. '54325aaaaaabbbbbb': 'readAndWrite',
  56. '': null,
  57. abc123def: null,
  58. }
  59. for (const token of Object.keys(specs)) {
  60. expect(this.TokenAccessHandler.getTokenType(token)).to.equal(
  61. specs[token]
  62. )
  63. }
  64. })
  65. })
  66. describe('getProjectByReadOnlyToken', function () {
  67. beforeEach(function () {
  68. this.token = 'abcdefabcdef'
  69. this.Project.findOne = sinon.stub().callsArgWith(2, null, this.project)
  70. })
  71. it('should get the project', function (done) {
  72. this.TokenAccessHandler.getProjectByReadOnlyToken(
  73. this.token,
  74. (err, project) => {
  75. expect(err).to.not.exist
  76. expect(project).to.exist
  77. expect(this.Project.findOne.callCount).to.equal(1)
  78. done()
  79. }
  80. )
  81. })
  82. })
  83. describe('getProjectByReadAndWriteToken', function () {
  84. beforeEach(function () {
  85. sinon.spy(this.Crypto, 'timingSafeEqual')
  86. this.token = '1234abcdefabcdef'
  87. this.project.tokens = {
  88. readAndWrite: this.token,
  89. readAndWritePrefix: '1234',
  90. }
  91. this.Project.findOne = sinon.stub().callsArgWith(2, null, this.project)
  92. })
  93. afterEach(function () {
  94. this.Crypto.timingSafeEqual.restore()
  95. })
  96. it('should get the project and do timing-safe comparison', function (done) {
  97. this.TokenAccessHandler.getProjectByReadAndWriteToken(
  98. this.token,
  99. (err, project) => {
  100. expect(err).to.not.exist
  101. expect(project).to.exist
  102. expect(this.Crypto.timingSafeEqual.callCount).to.equal(1)
  103. expect(
  104. this.Crypto.timingSafeEqual.calledWith(Buffer.from(this.token))
  105. ).to.equal(true)
  106. expect(this.Project.findOne.callCount).to.equal(1)
  107. done()
  108. }
  109. )
  110. })
  111. })
  112. describe('addReadOnlyUserToProject', function () {
  113. beforeEach(function () {
  114. return (this.Project.updateOne = sinon.stub().callsArgWith(2, null))
  115. })
  116. it('should call Project.updateOne', function (done) {
  117. return this.TokenAccessHandler.addReadOnlyUserToProject(
  118. this.userId,
  119. this.projectId,
  120. err => {
  121. expect(this.Project.updateOne.callCount).to.equal(1)
  122. expect(
  123. this.Project.updateOne.calledWith({
  124. _id: this.projectId,
  125. })
  126. ).to.equal(true)
  127. expect(
  128. this.Project.updateOne.lastCall.args[1].$addToSet
  129. ).to.have.keys('tokenAccessReadOnly_refs')
  130. sinon.assert.calledWith(
  131. this.Analytics.recordEventForUser,
  132. this.userId,
  133. 'project-joined',
  134. { mode: 'read-only' }
  135. )
  136. return done()
  137. }
  138. )
  139. })
  140. it('should not produce an error', function (done) {
  141. return this.TokenAccessHandler.addReadOnlyUserToProject(
  142. this.userId,
  143. this.projectId,
  144. err => {
  145. expect(err).to.not.exist
  146. return done()
  147. }
  148. )
  149. })
  150. describe('when Project.updateOne produces an error', function () {
  151. beforeEach(function () {
  152. return (this.Project.updateOne = sinon
  153. .stub()
  154. .callsArgWith(2, new Error('woops')))
  155. })
  156. it('should produce an error', function (done) {
  157. return this.TokenAccessHandler.addReadOnlyUserToProject(
  158. this.userId,
  159. this.projectId,
  160. err => {
  161. expect(err).to.exist
  162. return done()
  163. }
  164. )
  165. })
  166. })
  167. })
  168. describe('addReadAndWriteUserToProject', function () {
  169. beforeEach(function () {
  170. return (this.Project.updateOne = sinon.stub().callsArgWith(2, null))
  171. })
  172. it('should call Project.updateOne', function (done) {
  173. return this.TokenAccessHandler.addReadAndWriteUserToProject(
  174. this.userId,
  175. this.projectId,
  176. err => {
  177. expect(this.Project.updateOne.callCount).to.equal(1)
  178. expect(
  179. this.Project.updateOne.calledWith({
  180. _id: this.projectId,
  181. })
  182. ).to.equal(true)
  183. expect(
  184. this.Project.updateOne.lastCall.args[1].$addToSet
  185. ).to.have.keys('tokenAccessReadAndWrite_refs')
  186. sinon.assert.calledWith(
  187. this.Analytics.recordEventForUser,
  188. this.userId,
  189. 'project-joined',
  190. { mode: 'read-write' }
  191. )
  192. return done()
  193. }
  194. )
  195. })
  196. it('should not produce an error', function (done) {
  197. return this.TokenAccessHandler.addReadAndWriteUserToProject(
  198. this.userId,
  199. this.projectId,
  200. err => {
  201. expect(err).to.not.exist
  202. return done()
  203. }
  204. )
  205. })
  206. describe('when Project.updateOne produces an error', function () {
  207. beforeEach(function () {
  208. return (this.Project.updateOne = sinon
  209. .stub()
  210. .callsArgWith(2, new Error('woops')))
  211. })
  212. it('should produce an error', function (done) {
  213. return this.TokenAccessHandler.addReadAndWriteUserToProject(
  214. this.userId,
  215. this.projectId,
  216. err => {
  217. expect(err).to.exist
  218. return done()
  219. }
  220. )
  221. })
  222. })
  223. })
  224. describe('grantSessionTokenAccess', function () {
  225. beforeEach(function () {
  226. return (this.req = { session: {}, headers: {} })
  227. })
  228. it('should add the token to the session', function (done) {
  229. this.TokenAccessHandler.grantSessionTokenAccess(
  230. this.req,
  231. this.projectId,
  232. this.token
  233. )
  234. expect(
  235. this.req.session.anonTokenAccess[this.projectId.toString()]
  236. ).to.equal(this.token)
  237. return done()
  238. })
  239. })
  240. describe('validateTokenForAnonymousAccess', function () {
  241. describe('when a read-only project is found', function () {
  242. beforeEach(function () {
  243. this.TokenAccessHandler.getTokenType = sinon.stub().returns('readOnly')
  244. this.TokenAccessHandler.getProjectByToken = sinon
  245. .stub()
  246. .callsArgWith(2, null, this.project)
  247. })
  248. it('should try to find projects with both kinds of token', function (done) {
  249. return this.TokenAccessHandler.validateTokenForAnonymousAccess(
  250. this.projectId,
  251. this.token,
  252. (err, allowed) => {
  253. expect(
  254. this.TokenAccessHandler.getProjectByToken.callCount
  255. ).to.equal(1)
  256. return done()
  257. }
  258. )
  259. })
  260. it('should allow read-only access', function (done) {
  261. return this.TokenAccessHandler.validateTokenForAnonymousAccess(
  262. this.projectId,
  263. this.token,
  264. (err, rw, ro) => {
  265. expect(err).to.not.exist
  266. expect(rw).to.equal(false)
  267. expect(ro).to.equal(true)
  268. return done()
  269. }
  270. )
  271. })
  272. })
  273. describe('when a read-and-write project is found', function () {
  274. beforeEach(function () {
  275. this.TokenAccessHandler.getTokenType = sinon
  276. .stub()
  277. .returns('readAndWrite')
  278. this.TokenAccessHandler.getProjectByToken = sinon
  279. .stub()
  280. .callsArgWith(2, null, this.project)
  281. })
  282. describe('when Anonymous token access is not enabled', function (done) {
  283. beforeEach(function () {
  284. this.TokenAccessHandler.ANONYMOUS_READ_AND_WRITE_ENABLED = false
  285. })
  286. it('should try to find projects with both kinds of token', function (done) {
  287. return this.TokenAccessHandler.validateTokenForAnonymousAccess(
  288. this.projectId,
  289. this.token,
  290. (err, rw, ro) => {
  291. expect(
  292. this.TokenAccessHandler.getProjectByToken.callCount
  293. ).to.equal(1)
  294. return done()
  295. }
  296. )
  297. })
  298. it('should not allow read-and-write access', function (done) {
  299. return this.TokenAccessHandler.validateTokenForAnonymousAccess(
  300. this.projectId,
  301. this.token,
  302. (err, rw, ro) => {
  303. expect(err).to.not.exist
  304. expect(rw).to.equal(false)
  305. expect(ro).to.equal(false)
  306. return done()
  307. }
  308. )
  309. })
  310. })
  311. describe('when anonymous token access is enabled', function (done) {
  312. beforeEach(function () {
  313. this.TokenAccessHandler.ANONYMOUS_READ_AND_WRITE_ENABLED = true
  314. })
  315. it('should try to find projects with both kinds of token', function (done) {
  316. return this.TokenAccessHandler.validateTokenForAnonymousAccess(
  317. this.projectId,
  318. this.token,
  319. (err, rw, ro) => {
  320. expect(
  321. this.TokenAccessHandler.getProjectByToken.callCount
  322. ).to.equal(1)
  323. return done()
  324. }
  325. )
  326. })
  327. it('should allow read-and-write access', function (done) {
  328. return this.TokenAccessHandler.validateTokenForAnonymousAccess(
  329. this.projectId,
  330. this.token,
  331. (err, rw, ro) => {
  332. expect(err).to.not.exist
  333. expect(rw).to.equal(true)
  334. expect(ro).to.equal(false)
  335. return done()
  336. }
  337. )
  338. })
  339. })
  340. })
  341. describe('when no project is found', function () {
  342. beforeEach(function () {
  343. this.TokenAccessHandler.getProjectByToken = sinon
  344. .stub()
  345. .callsArgWith(2, null, null, null)
  346. })
  347. it('should try to find projects with both kinds of token', function (done) {
  348. return this.TokenAccessHandler.validateTokenForAnonymousAccess(
  349. this.projectId,
  350. this.token,
  351. (err, allowed) => {
  352. expect(
  353. this.TokenAccessHandler.getProjectByToken.callCount
  354. ).to.equal(1)
  355. return done()
  356. }
  357. )
  358. })
  359. it('should not allow any access', function (done) {
  360. return this.TokenAccessHandler.validateTokenForAnonymousAccess(
  361. this.projectId,
  362. this.token,
  363. (err, rw, ro) => {
  364. expect(err).to.not.exist
  365. expect(rw).to.equal(false)
  366. expect(ro).to.equal(false)
  367. return done()
  368. }
  369. )
  370. })
  371. })
  372. describe('when findProject produces an error', function () {
  373. beforeEach(function () {
  374. this.TokenAccessHandler.getProjectByToken = sinon
  375. .stub()
  376. .callsArgWith(2, new Error('woops'))
  377. })
  378. it('should try to find projects with both kinds of token', function (done) {
  379. return this.TokenAccessHandler.validateTokenForAnonymousAccess(
  380. this.projectId,
  381. this.token,
  382. (err, allowed) => {
  383. expect(
  384. this.TokenAccessHandler.getProjectByToken.callCount
  385. ).to.equal(1)
  386. return done()
  387. }
  388. )
  389. })
  390. it('should produce an error and not allow access', function (done) {
  391. return this.TokenAccessHandler.validateTokenForAnonymousAccess(
  392. this.projectId,
  393. this.token,
  394. (err, rw, ro) => {
  395. expect(err).to.exist
  396. expect(err).to.be.instanceof(Error)
  397. expect(rw).to.equal(undefined)
  398. expect(ro).to.equal(undefined)
  399. return done()
  400. }
  401. )
  402. })
  403. })
  404. describe('when project is not set to token-based access', function () {
  405. beforeEach(function () {
  406. return (this.project.publicAccesLevel = 'private')
  407. })
  408. describe('for read-and-write project', function () {
  409. beforeEach(function () {
  410. this.TokenAccessHandler.getTokenType = sinon
  411. .stub()
  412. .returns('readAndWrite')
  413. this.TokenAccessHandler.getProjectByToken = sinon
  414. .stub()
  415. .callsArgWith(2, null, this.project)
  416. })
  417. it('should not allow any access', function (done) {
  418. return this.TokenAccessHandler.validateTokenForAnonymousAccess(
  419. this.projectId,
  420. this.token,
  421. (err, rw, ro) => {
  422. expect(err).to.not.exist
  423. expect(rw).to.equal(false)
  424. expect(ro).to.equal(false)
  425. return done()
  426. }
  427. )
  428. })
  429. })
  430. describe('for read-only project', function () {
  431. beforeEach(function () {
  432. this.TokenAccessHandler.getTokenType = sinon
  433. .stub()
  434. .returns('readOnly')
  435. this.TokenAccessHandler.getProjectByToken = sinon
  436. .stub()
  437. .callsArgWith(2, null, this.project)
  438. })
  439. it('should not allow any access', function (done) {
  440. return this.TokenAccessHandler.validateTokenForAnonymousAccess(
  441. this.projectId,
  442. this.token,
  443. (err, rw, ro) => {
  444. expect(err).to.not.exist
  445. expect(rw).to.equal(false)
  446. expect(ro).to.equal(false)
  447. return done()
  448. }
  449. )
  450. })
  451. })
  452. describe('with nothing', function () {
  453. beforeEach(function () {
  454. this.TokenAccessHandler.getProjectByToken = sinon
  455. .stub()
  456. .callsArgWith(1, null, null, null)
  457. })
  458. it('should not allow any access', function (done) {
  459. return this.TokenAccessHandler.validateTokenForAnonymousAccess(
  460. this.projectId,
  461. null,
  462. (err, rw, ro) => {
  463. expect(err).to.not.exist
  464. expect(rw).to.equal(false)
  465. expect(ro).to.equal(false)
  466. return done()
  467. }
  468. )
  469. })
  470. })
  471. })
  472. })
  473. describe('protectTokens', function () {
  474. beforeEach(function () {
  475. return (this.project = {
  476. tokens: {
  477. readAndWrite: 'rw',
  478. readOnly: 'ro',
  479. readAndWritePrefix: 'pre',
  480. },
  481. })
  482. })
  483. it('should hide write token from read-only user', function () {
  484. this.TokenAccessHandler.protectTokens(this.project, 'readOnly')
  485. expect(this.project.tokens.readAndWrite).to.equal('')
  486. expect(this.project.tokens.readAndWritePrefix).to.equal('')
  487. return expect(this.project.tokens.readOnly).to.equal('ro')
  488. })
  489. it('should hide read token from read-write user', function () {
  490. this.TokenAccessHandler.protectTokens(this.project, 'readAndWrite')
  491. expect(this.project.tokens.readAndWrite).to.equal('rw')
  492. return expect(this.project.tokens.readOnly).to.equal('')
  493. })
  494. it('should leave tokens in place for owner', function () {
  495. this.TokenAccessHandler.protectTokens(this.project, 'owner')
  496. expect(this.project.tokens.readAndWrite).to.equal('rw')
  497. return expect(this.project.tokens.readOnly).to.equal('ro')
  498. })
  499. })
  500. describe('getDocPublishedInfo', function () {
  501. beforeEach(function () {
  502. return (this.callback = sinon.stub())
  503. })
  504. describe('when v1 api not set', function () {
  505. beforeEach(function () {
  506. this.settings.apis = { v1: undefined }
  507. return this.TokenAccessHandler.getV1DocPublishedInfo(
  508. this.token,
  509. this.callback
  510. )
  511. })
  512. it('should not check access and return default info', function () {
  513. expect(this.V1Api.request.called).to.equal(false)
  514. return expect(
  515. this.callback.calledWith(null, {
  516. allow: true,
  517. })
  518. ).to.equal(true)
  519. })
  520. })
  521. describe('when v1 api is set', function () {
  522. beforeEach(function () {
  523. return (this.settings.apis = { v1: { url: 'v1Url' } })
  524. })
  525. describe('on V1Api.request success', function () {
  526. beforeEach(function () {
  527. this.V1Api.request = sinon
  528. .stub()
  529. .callsArgWith(1, null, null, 'mock-data')
  530. return this.TokenAccessHandler.getV1DocPublishedInfo(
  531. this.token,
  532. this.callback
  533. )
  534. })
  535. it('should return response body', function () {
  536. expect(
  537. this.V1Api.request.calledWith({
  538. url: `/api/v1/sharelatex/docs/${this.token}/is_published`,
  539. })
  540. ).to.equal(true)
  541. return expect(this.callback.calledWith(null, 'mock-data')).to.equal(
  542. true
  543. )
  544. })
  545. })
  546. describe('on V1Api.request error', function () {
  547. beforeEach(function () {
  548. this.V1Api.request = sinon.stub().callsArgWith(1, 'error')
  549. return this.TokenAccessHandler.getV1DocPublishedInfo(
  550. this.token,
  551. this.callback
  552. )
  553. })
  554. it('should callback with error', function () {
  555. return expect(this.callback.calledWith('error')).to.equal(true)
  556. })
  557. })
  558. })
  559. })
  560. describe('getV1DocInfo', function () {
  561. beforeEach(function () {
  562. return (this.callback = sinon.stub())
  563. })
  564. describe('when v1 api not set', function () {
  565. beforeEach(function () {
  566. return this.TokenAccessHandler.getV1DocInfo(
  567. this.token,
  568. this.v2UserId,
  569. this.callback
  570. )
  571. })
  572. it('should not check access and return default info', function () {
  573. expect(this.V1Api.request.called).to.equal(false)
  574. return expect(
  575. this.callback.calledWith(null, {
  576. exists: true,
  577. exported: false,
  578. })
  579. ).to.equal(true)
  580. })
  581. })
  582. describe('when v1 api is set', function () {
  583. beforeEach(function () {
  584. return (this.settings.apis = { v1: 'v1' })
  585. })
  586. describe('on V1Api.request success', function () {
  587. beforeEach(function () {
  588. this.V1Api.request = sinon
  589. .stub()
  590. .callsArgWith(1, null, null, 'mock-data')
  591. return this.TokenAccessHandler.getV1DocInfo(
  592. this.token,
  593. this.v2UserId,
  594. this.callback
  595. )
  596. })
  597. it('should return response body', function () {
  598. expect(
  599. this.V1Api.request.calledWith({
  600. url: `/api/v1/sharelatex/docs/${this.token}/info`,
  601. })
  602. ).to.equal(true)
  603. return expect(this.callback.calledWith(null, 'mock-data')).to.equal(
  604. true
  605. )
  606. })
  607. })
  608. describe('on V1Api.request error', function () {
  609. beforeEach(function () {
  610. this.V1Api.request = sinon.stub().callsArgWith(1, 'error')
  611. return this.TokenAccessHandler.getV1DocInfo(
  612. this.token,
  613. this.v2UserId,
  614. this.callback
  615. )
  616. })
  617. it('should callback with error', function () {
  618. return expect(this.callback.calledWith('error')).to.equal(true)
  619. })
  620. })
  621. })
  622. })
  623. })