AuthorizationTests.mjs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640
  1. import { expect } from 'chai'
  2. import UserHelper from './helpers/User.mjs'
  3. import request from './helpers/request.js'
  4. import settings from '@overleaf/settings'
  5. import Features from '../../../app/src/infrastructure/Features.js'
  6. import expectErrorResponse from './helpers/expectErrorResponse.mjs'
  7. import { promisify } from '@overleaf/promise-utils'
  8. const User = UserHelper.promises
  9. async function tryReadAccess(user, projectId, test) {
  10. const projectRequest = await user.doRequest('get', `/project/${projectId}`)
  11. test(projectRequest.response, projectRequest.body)
  12. const zipRequest = await user.doRequest(
  13. 'get',
  14. `/project/${projectId}/download/zip`
  15. )
  16. test(zipRequest.response, zipRequest.body)
  17. }
  18. async function tryRenameProjectAccess(user, projectId, test) {
  19. const { response, body } = await user.doRequest('post', {
  20. url: `/project/${projectId}/settings`,
  21. json: { name: 'new name' },
  22. })
  23. test(response, body)
  24. }
  25. async function trySettingsWriteAccess(user, projectId, test) {
  26. const { response, body } = await user.doRequest('post', {
  27. url: `/project/${projectId}/settings`,
  28. json: { compiler: 'latex' },
  29. })
  30. test(response, body)
  31. }
  32. async function tryProjectAdminAccess(user, projectId, test) {
  33. const renameRequest = await user.doRequest('post', {
  34. url: `/project/${projectId}/rename`,
  35. json: { newProjectName: 'new-name' },
  36. })
  37. test(renameRequest.response, renameRequest.body)
  38. const settingsRequest = await user.doRequest('post', {
  39. url: `/project/${projectId}/settings/admin`,
  40. json: { publicAccessLevel: 'private' },
  41. })
  42. test(settingsRequest.response, settingsRequest.body)
  43. }
  44. async function tryAdminAccess(user, test) {
  45. const { response, body } = await user.doRequest('get', '/admin')
  46. test(response, body)
  47. if (Features.hasFeature('saas')) {
  48. const { response, body } = await user.doRequest(
  49. 'get',
  50. `/admin/user/${user._id}`
  51. )
  52. test(response, body)
  53. }
  54. }
  55. function tryContentAccessCb(user, projectId, test, callback) {
  56. // The real-time service calls this end point to determine the user's
  57. // permissions.
  58. let userId
  59. if (user.id != null) {
  60. userId = user.id
  61. } else {
  62. userId = 'anonymous-user'
  63. }
  64. request.post(
  65. {
  66. url: `/project/${projectId}/join`,
  67. auth: {
  68. user: settings.apis.web.user,
  69. pass: settings.apis.web.pass,
  70. sendImmediately: true,
  71. },
  72. json: { userId },
  73. jar: false,
  74. },
  75. (error, response, body) => {
  76. if (error != null) {
  77. return callback(error)
  78. }
  79. test(response, body)
  80. callback()
  81. }
  82. )
  83. }
  84. const tryContentAccess = promisify(tryContentAccessCb)
  85. async function expectAdminAccess(user) {
  86. await tryAdminAccess(user, response =>
  87. expect(response.statusCode).to.be.oneOf([200, 204])
  88. )
  89. }
  90. async function expectRedirectedAdminAccess(user) {
  91. await tryAdminAccess(user, response => {
  92. expect(response.statusCode).to.equal(302)
  93. expect(response.headers.location).to.equal(
  94. settings.adminUrl + response.request.uri.pathname
  95. )
  96. })
  97. }
  98. async function expectReadAccess(user, projectId) {
  99. await tryReadAccess(user, projectId, response =>
  100. expect(response.statusCode).to.be.oneOf([200, 204])
  101. )
  102. await tryContentAccess(user, projectId, (response, body) =>
  103. expect(body.privilegeLevel).to.be.oneOf([
  104. 'owner',
  105. 'readAndWrite',
  106. 'readOnly',
  107. ])
  108. )
  109. }
  110. async function expectContentWriteAccess(user, projectId) {
  111. await tryContentAccess(user, projectId, (response, body) =>
  112. expect(body.privilegeLevel).to.be.oneOf(['owner', 'readAndWrite'])
  113. )
  114. }
  115. async function expectRenameProjectAccess(user, projectId) {
  116. await tryRenameProjectAccess(user, projectId, response => {
  117. expect(response.statusCode).to.be.oneOf([200, 204])
  118. })
  119. }
  120. async function expectSettingsWriteAccess(user, projectId) {
  121. await trySettingsWriteAccess(user, projectId, response =>
  122. expect(response.statusCode).to.be.oneOf([200, 204])
  123. )
  124. }
  125. async function expectProjectAdminAccess(user, projectId) {
  126. await tryProjectAdminAccess(user, projectId, response =>
  127. expect(response.statusCode).to.be.oneOf([200, 204])
  128. )
  129. }
  130. async function expectNoReadAccess(user, projectId) {
  131. await tryReadAccess(user, projectId, expectErrorResponse.restricted.html)
  132. await tryContentAccess(user, projectId, (response, body) => {
  133. expect(response.statusCode).to.equal(403)
  134. expect(body).to.equal('Forbidden')
  135. })
  136. }
  137. async function expectNoContentWriteAccess(user, projectId) {
  138. await tryContentAccess(user, projectId, (response, body) =>
  139. expect(body.privilegeLevel).to.be.oneOf([undefined, null, 'readOnly'])
  140. )
  141. }
  142. async function expectNoSettingsWriteAccess(user, projectId) {
  143. await trySettingsWriteAccess(
  144. user,
  145. projectId,
  146. expectErrorResponse.restricted.json
  147. )
  148. }
  149. async function expectNoRenameProjectAccess(user, projectId) {
  150. await tryRenameProjectAccess(
  151. user,
  152. projectId,
  153. expectErrorResponse.restricted.json
  154. )
  155. }
  156. async function expectNoProjectAdminAccess(user, projectId) {
  157. await tryProjectAdminAccess(user, projectId, response => {
  158. expect(response.statusCode).to.equal(403)
  159. })
  160. }
  161. async function expectNoAnonymousProjectAdminAccess(user, projectId) {
  162. await tryProjectAdminAccess(
  163. user,
  164. projectId,
  165. expectErrorResponse.requireLogin.json
  166. )
  167. }
  168. async function expectChatAccess(user, projectId) {
  169. const { response } = await user.doRequest(
  170. 'get',
  171. `/project/${projectId}/messages`
  172. )
  173. expect(response.statusCode).to.equal(200)
  174. }
  175. async function expectNoChatAccess(user, projectId) {
  176. const { response } = await user.doRequest(
  177. 'get',
  178. `/project/${projectId}/messages`
  179. )
  180. expect(response.statusCode).to.equal(403)
  181. }
  182. describe('Authorization', function () {
  183. beforeEach(async function () {
  184. this.timeout(90000)
  185. this.owner = new User()
  186. this.other1 = new User()
  187. this.other2 = new User()
  188. this.anon = new User()
  189. this.site_admin = new User({ email: 'admin@example.com' })
  190. settings.adminRolesEnabled = false
  191. await Promise.all([
  192. this.owner.login(),
  193. this.other1.login(),
  194. this.other2.login(),
  195. this.anon.getCsrfToken(),
  196. (async () => {
  197. await this.site_admin.ensureUserExists()
  198. await this.site_admin.ensureAdmin()
  199. await this.site_admin.login()
  200. })(),
  201. ])
  202. })
  203. describe('private project', function () {
  204. beforeEach(async function () {
  205. this.projectId = await this.owner.createProject('private-project')
  206. })
  207. it('should allow the owner read access to it', async function () {
  208. await expectReadAccess(this.owner, this.projectId)
  209. })
  210. it('should allow the owner write access to its content', async function () {
  211. await expectContentWriteAccess(this.owner, this.projectId)
  212. })
  213. it('should allow the owner write access to its settings', async function () {
  214. await expectSettingsWriteAccess(this.owner, this.projectId)
  215. })
  216. it('should allow the owner to rename the project', async function () {
  217. await expectRenameProjectAccess(this.owner, this.projectId)
  218. })
  219. it('should allow the owner project admin access to it', async function () {
  220. await expectProjectAdminAccess(this.owner, this.projectId)
  221. })
  222. it('should allow the owner user chat messages access', async function () {
  223. await expectChatAccess(this.owner, this.projectId)
  224. })
  225. it('should not allow another user read access to the project', async function () {
  226. await expectNoReadAccess(this.other1, this.projectId)
  227. })
  228. it('should not allow another user write access to its content', async function () {
  229. await expectNoContentWriteAccess(this.other1, this.projectId)
  230. })
  231. it('should not allow another user write access to its settings', async function () {
  232. await expectNoSettingsWriteAccess(this.other1, this.projectId)
  233. })
  234. it('should not allow another user to rename the project', async function () {
  235. await expectNoRenameProjectAccess(this.other1, this.projectId)
  236. })
  237. it('should not allow another user project admin access to it', async function () {
  238. await expectNoProjectAdminAccess(this.other1, this.projectId)
  239. })
  240. it('should not allow another user chat messages access', async function () {
  241. await expectNoChatAccess(this.other1, this.projectId)
  242. })
  243. it('should not allow anonymous user read access to it', async function () {
  244. await expectNoReadAccess(this.anon, this.projectId)
  245. })
  246. it('should not allow anonymous user write access to its content', async function () {
  247. await expectNoContentWriteAccess(this.anon, this.projectId)
  248. })
  249. it('should not allow anonymous user write access to its settings', async function () {
  250. await expectNoSettingsWriteAccess(this.anon, this.projectId)
  251. })
  252. it('should not allow anonymous user to rename the project', async function () {
  253. await expectNoRenameProjectAccess(this.anon, this.projectId)
  254. })
  255. it('should not allow anonymous user project admin access to it', async function () {
  256. await expectNoAnonymousProjectAdminAccess(this.anon, this.projectId)
  257. })
  258. it('should not allow anonymous user chat messages access', async function () {
  259. await expectNoChatAccess(this.anon, this.projectId)
  260. })
  261. describe('with admin privilege available', function () {
  262. beforeEach(function () {
  263. settings.adminPrivilegeAvailable = true
  264. })
  265. it('should allow site admin users read access to it', async function () {
  266. await expectReadAccess(this.site_admin, this.projectId)
  267. })
  268. it('should allow site admin users write access to its content', async function () {
  269. await expectContentWriteAccess(this.site_admin, this.projectId)
  270. })
  271. it('should allow site admin users write access to its settings', async function () {
  272. await expectSettingsWriteAccess(this.site_admin, this.projectId)
  273. })
  274. it('should allow site admin users to rename the project', async function () {
  275. await expectRenameProjectAccess(this.site_admin, this.projectId)
  276. })
  277. it('should allow site admin users project admin access to it', async function () {
  278. await expectProjectAdminAccess(this.site_admin, this.projectId)
  279. })
  280. it('should allow site admin users site admin access to site admin endpoints', async function () {
  281. await expectAdminAccess(this.site_admin)
  282. })
  283. })
  284. describe('with admin privilege unavailable', function () {
  285. beforeEach(function () {
  286. settings.adminPrivilegeAvailable = false
  287. })
  288. afterEach(function () {
  289. settings.adminPrivilegeAvailable = true
  290. })
  291. it('should not allow site admin users read access to it', async function () {
  292. await expectNoReadAccess(this.site_admin, this.projectId)
  293. })
  294. it('should not allow site admin users write access to its content', async function () {
  295. await expectNoContentWriteAccess(this.site_admin, this.projectId)
  296. })
  297. it('should not allow site admin users write access to its settings', async function () {
  298. await expectNoSettingsWriteAccess(this.site_admin, this.projectId)
  299. })
  300. it('should not allow site admin users to rename the project', async function () {
  301. await expectNoRenameProjectAccess(this.site_admin, this.projectId)
  302. })
  303. it('should not allow site admin users project admin access to it', async function () {
  304. await expectNoProjectAdminAccess(this.site_admin, this.projectId)
  305. })
  306. it('should redirect site admin users when accessing site admin endpoints', async function () {
  307. await expectRedirectedAdminAccess(this.site_admin)
  308. })
  309. })
  310. describe('with admin roles', function () {
  311. beforeEach(function () {
  312. if (!settings.moduleImportSequence.includes('admin-roles')) {
  313. this.skip()
  314. }
  315. settings.adminRolesEnabled = true
  316. settings.adminPrivilegeAvailable = true
  317. })
  318. afterEach(function () {
  319. settings.adminRolesEnabled = false
  320. settings.adminPrivilegeAvailable = true
  321. this.site_admin.mongoUpdate({
  322. $set: { adminRoles: [] },
  323. })
  324. })
  325. describe('engineering', function () {
  326. beforeEach(function () {
  327. this.site_admin.mongoUpdate({
  328. $set: { adminRoles: ['engineering'] },
  329. })
  330. })
  331. it('should allow site admin users read access to it', async function () {
  332. await expectReadAccess(this.site_admin, this.projectId)
  333. })
  334. it('should not allow site admin users write access to its content', async function () {
  335. await expectNoContentWriteAccess(this.site_admin, this.projectId)
  336. })
  337. it('should allow site admin users write access to its settings', async function () {
  338. await expectSettingsWriteAccess(this.site_admin, this.projectId)
  339. })
  340. it('should allow site admin users to rename the project', async function () {
  341. await expectRenameProjectAccess(this.site_admin, this.projectId)
  342. })
  343. it('should allow site admin users project admin access to it', async function () {
  344. await expectProjectAdminAccess(this.site_admin, this.projectId)
  345. })
  346. it('should allow site admin users site admin access to site admin endpoints', async function () {
  347. await expectAdminAccess(this.site_admin)
  348. })
  349. })
  350. describe('no admin role assigned', function () {
  351. beforeEach(function () {
  352. this.site_admin.mongoUpdate({
  353. $set: { adminRoles: [] },
  354. })
  355. })
  356. it('should not allow site admin users read access to it', async function () {
  357. await expectNoReadAccess(this.site_admin, this.projectId)
  358. })
  359. it('should not allow site admin users write access to its content', async function () {
  360. await expectNoContentWriteAccess(this.site_admin, this.projectId)
  361. })
  362. it('should not allow site admin users write access to its settings', async function () {
  363. await expectNoSettingsWriteAccess(this.site_admin, this.projectId)
  364. })
  365. it('should not allow site admin users to rename the project', async function () {
  366. await expectNoRenameProjectAccess(this.site_admin, this.projectId)
  367. })
  368. it('should not allow site admin users project admin access to it', async function () {
  369. await expectNoProjectAdminAccess(this.site_admin, this.projectId)
  370. })
  371. it('should allow site admin users site admin access to site admin endpoints', async function () {
  372. await expectAdminAccess(this.site_admin)
  373. })
  374. })
  375. })
  376. })
  377. describe('shared project', function () {
  378. beforeEach(async function () {
  379. this.rw_user = this.other1
  380. this.ro_user = this.other2
  381. this.projectId = await this.owner.createProject('private-project')
  382. await this.owner.addUserToProject(
  383. this.projectId,
  384. this.ro_user,
  385. 'readOnly'
  386. )
  387. await this.owner.addUserToProject(
  388. this.projectId,
  389. this.rw_user,
  390. 'readAndWrite'
  391. )
  392. })
  393. it('should allow the read-only user read access to it', async function () {
  394. await expectReadAccess(this.ro_user, this.projectId)
  395. })
  396. it('should allow the read-only user chat messages access', async function () {
  397. await expectChatAccess(this.ro_user, this.projectId)
  398. })
  399. it('should not allow the read-only user write access to its content', async function () {
  400. await expectNoContentWriteAccess(this.ro_user, this.projectId)
  401. })
  402. it('should not allow the read-only user write access to its settings', async function () {
  403. await expectNoSettingsWriteAccess(this.ro_user, this.projectId)
  404. })
  405. it('should not allow the read-only user to rename the project', async function () {
  406. await expectNoRenameProjectAccess(this.ro_user, this.projectId)
  407. })
  408. it('should not allow the read-only user project admin access to it', async function () {
  409. await expectNoProjectAdminAccess(this.ro_user, this.projectId)
  410. })
  411. it('should allow the read-write user read access to it', async function () {
  412. await expectReadAccess(this.rw_user, this.projectId)
  413. })
  414. it('should allow the read-write user write access to its content', async function () {
  415. await expectContentWriteAccess(this.rw_user, this.projectId)
  416. })
  417. it('should allow the read-write user write access to its settings', async function () {
  418. await expectSettingsWriteAccess(this.rw_user, this.projectId)
  419. })
  420. it('should not allow the read-write user to rename the project', async function () {
  421. await expectNoRenameProjectAccess(this.rw_user, this.projectId)
  422. })
  423. it('should not allow the read-write user project admin access to it', async function () {
  424. await expectNoProjectAdminAccess(this.rw_user, this.projectId)
  425. })
  426. it('should allow the read-write user chat messages access', async function () {
  427. await expectChatAccess(this.rw_user, this.projectId)
  428. })
  429. })
  430. describe('public read-write project', function () {
  431. /**
  432. * Note: this is a test for the legacy "public access" feature.
  433. * See documentation comment in `Authorization/PublicAccessLevels`
  434. * */
  435. beforeEach(async function () {
  436. this.projectId = await this.owner.createProject('public-rw-project')
  437. await this.owner.makePublic(this.projectId, 'readAndWrite')
  438. })
  439. it('should allow a user read access to it', async function () {
  440. await expectReadAccess(this.other1, this.projectId)
  441. })
  442. it('should allow a user write access to its content', async function () {
  443. await expectContentWriteAccess(this.other1, this.projectId)
  444. })
  445. it('should allow a user chat messages access', async function () {
  446. await expectChatAccess(this.other1, this.projectId)
  447. })
  448. it('should not allow a user write access to its settings', async function () {
  449. await expectNoSettingsWriteAccess(this.other1, this.projectId)
  450. })
  451. it('should not allow a user to rename the project', async function () {
  452. await expectNoRenameProjectAccess(this.other1, this.projectId)
  453. })
  454. it('should not allow a user project admin access to it', async function () {
  455. await expectNoProjectAdminAccess(this.other1, this.projectId)
  456. })
  457. it('should allow an anonymous user read access to it', async function () {
  458. await expectReadAccess(this.anon, this.projectId)
  459. })
  460. it('should allow an anonymous user write access to its content', async function () {
  461. await expectContentWriteAccess(this.anon, this.projectId)
  462. })
  463. it('should allow an anonymous user chat messages access', async function () {
  464. // chat access for anonymous users is a CE/SP-only feature, although currently broken
  465. // https://github.com/overleaf/internal/issues/10944
  466. if (Features.hasFeature('saas')) {
  467. this.skip()
  468. }
  469. await expectChatAccess(this.anon, this.projectId)
  470. })
  471. it('should not allow an anonymous user write access to its settings', async function () {
  472. await expectNoSettingsWriteAccess(this.anon, this.projectId)
  473. })
  474. it('should not allow an anonymous user to rename the project', async function () {
  475. await expectNoRenameProjectAccess(this.anon, this.projectId)
  476. })
  477. it('should not allow an anonymous user project admin access to it', async function () {
  478. await expectNoAnonymousProjectAdminAccess(this.anon, this.projectId)
  479. })
  480. })
  481. describe('public read-only project', function () {
  482. /**
  483. * Note: this is a test for the legacy "public access" feature.
  484. * See documentation comment in `Authorization/PublicAccessLevels`
  485. * */
  486. beforeEach(async function () {
  487. this.projectId = await this.owner.createProject('public-ro-project')
  488. await this.owner.makePublic(this.projectId, 'readOnly')
  489. })
  490. it('should allow a user read access to it', async function () {
  491. await expectReadAccess(this.other1, this.projectId)
  492. })
  493. it('should not allow a user write access to its content', async function () {
  494. await expectNoContentWriteAccess(this.other1, this.projectId)
  495. })
  496. it('should not allow a user write access to its settings', async function () {
  497. await expectNoSettingsWriteAccess(this.other1, this.projectId)
  498. })
  499. it('should not allow a user to rename the project', async function () {
  500. await expectNoRenameProjectAccess(this.other1, this.projectId)
  501. })
  502. it('should not allow a user project admin access to it', async function () {
  503. await expectNoProjectAdminAccess(this.other1, this.projectId)
  504. })
  505. // NOTE: legacy readOnly access does not count as 'restricted' in the new model
  506. it('should allow a user chat messages access', async function () {
  507. await expectChatAccess(this.other1, this.projectId)
  508. })
  509. it('should allow an anonymous user read access to it', async function () {
  510. await expectReadAccess(this.anon, this.projectId)
  511. })
  512. it('should not allow an anonymous user write access to its content', async function () {
  513. await expectNoContentWriteAccess(this.anon, this.projectId)
  514. })
  515. it('should not allow an anonymous user write access to its settings', async function () {
  516. await expectNoSettingsWriteAccess(this.anon, this.projectId)
  517. })
  518. it('should not allow an anonymous user to rename the project', async function () {
  519. await expectNoRenameProjectAccess(this.anon, this.projectId)
  520. })
  521. it('should not allow an anonymous user project admin access to it', async function () {
  522. await expectNoAnonymousProjectAdminAccess(this.anon, this.projectId)
  523. })
  524. it('should not allow an anonymous user chat messages access', async function () {
  525. await expectNoChatAccess(this.anon, this.projectId)
  526. })
  527. })
  528. })