TokenAccessHandlerTests.js 21 KB

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