AuthorizationMiddleware.test.mjs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493
  1. import { vi, expect } from 'vitest'
  2. import sinon from 'sinon'
  3. import mongodb from 'mongodb-legacy'
  4. import Errors from '../../../../app/src/Features/Errors/Errors.js'
  5. const { ObjectId } = mongodb
  6. const MODULE_PATH =
  7. '../../../../app/src/Features/Authorization/AuthorizationMiddleware.mjs'
  8. describe('AuthorizationMiddleware', function () {
  9. beforeEach(async function (ctx) {
  10. ctx.userId = new ObjectId().toString()
  11. ctx.project_id = new ObjectId().toString()
  12. ctx.doc_id = new ObjectId().toString()
  13. ctx.thread_id = new ObjectId().toString()
  14. ctx.token = 'some-token'
  15. ctx.AuthenticationController = {}
  16. ctx.SessionManager = {
  17. getSessionUser: sinon.stub().returns(null),
  18. getLoggedInUserId: sinon.stub().returns(ctx.userId),
  19. isUserLoggedIn: sinon.stub().returns(true),
  20. }
  21. ctx.AuthorizationManager = {
  22. promises: {
  23. canUserReadProject: sinon.stub(),
  24. canUserWriteProjectSettings: sinon.stub(),
  25. canUserWriteProjectContent: sinon.stub(),
  26. canUserWriteOrReviewProjectContent: sinon.stub(),
  27. canUserDeleteOrResolveThread: sinon.stub(),
  28. canUserAdminProject: sinon.stub(),
  29. canUserRenameProject: sinon.stub(),
  30. isUserSiteAdmin: sinon.stub(),
  31. isRestrictedUserForProject: sinon.stub(),
  32. },
  33. }
  34. ctx.HttpErrorHandler = {
  35. forbidden: sinon.stub(),
  36. }
  37. ctx.TokenAccessHandler = {
  38. getRequestToken: sinon.stub().returns(ctx.token),
  39. }
  40. ctx.DocumentUpdaterHandler = {
  41. promises: {
  42. getComment: sinon.stub().resolves(),
  43. },
  44. }
  45. vi.doMock('../../../../app/src/Features/Errors/Errors.js', () => ({
  46. default: Errors,
  47. }))
  48. vi.doMock(
  49. '../../../../app/src/Features/Authorization/AuthorizationManager',
  50. () => ({
  51. default: ctx.AuthorizationManager,
  52. })
  53. )
  54. vi.doMock('../../../../app/src/Features/Errors/HttpErrorHandler', () => ({
  55. default: ctx.HttpErrorHandler,
  56. }))
  57. vi.doMock('mongodb-legacy', () => ({
  58. default: { ObjectId },
  59. }))
  60. vi.doMock(
  61. '../../../../app/src/Features/Authentication/AuthenticationController',
  62. () => ({
  63. default: ctx.AuthenticationController,
  64. })
  65. )
  66. vi.doMock(
  67. '../../../../app/src/Features/Authentication/SessionManager',
  68. () => ({
  69. default: ctx.SessionManager,
  70. })
  71. )
  72. vi.doMock(
  73. '../../../../app/src/Features/TokenAccess/TokenAccessHandler',
  74. () => ({
  75. default: ctx.TokenAccessHandler,
  76. })
  77. )
  78. vi.doMock(
  79. '../../../../app/src/Features/Helpers/AdminAuthorizationHelper',
  80. () => ({
  81. default: {
  82. canRedirectToAdminDomain: sinon.stub().returns(false),
  83. },
  84. })
  85. )
  86. vi.doMock(
  87. '../../../../app/src/Features/DocumentUpdater/DocumentUpdaterHandler',
  88. () => ({
  89. default: ctx.DocumentUpdaterHandler,
  90. })
  91. )
  92. ctx.AuthorizationMiddleware = (await import(MODULE_PATH)).default
  93. ctx.req = {
  94. params: {
  95. project_id: ctx.project_id,
  96. },
  97. body: {},
  98. }
  99. ctx.res = {
  100. redirect: sinon.stub(),
  101. locals: {
  102. currentUrl: '/current/url',
  103. },
  104. }
  105. ctx.next = sinon.stub()
  106. })
  107. describe('ensureCanReadProject', function () {
  108. testMiddleware('ensureUserCanReadProject', 'canUserReadProject')
  109. })
  110. describe('ensureUserCanWriteProjectContent', function () {
  111. testMiddleware(
  112. 'ensureUserCanWriteProjectContent',
  113. 'canUserWriteProjectContent'
  114. )
  115. })
  116. describe('ensureUserCanWriteOrReviewProjectContent', function () {
  117. testMiddleware(
  118. 'ensureUserCanWriteOrReviewProjectContent',
  119. 'canUserWriteOrReviewProjectContent'
  120. )
  121. })
  122. describe('ensureUserCanDeleteOrResolveThread', function () {
  123. beforeEach(function (ctx) {
  124. ctx.req.params.thread_id = ctx.thread_id
  125. })
  126. describe('when user has permission', function () {
  127. beforeEach(function (ctx) {
  128. ctx.AuthorizationManager.promises.canUserDeleteOrResolveThread
  129. .withArgs(ctx.userId, ctx.project_id, ctx.thread_id, ctx.token)
  130. .resolves(true)
  131. })
  132. invokeMiddleware('ensureUserCanDeleteOrResolveThread')
  133. expectNext()
  134. })
  135. describe("when user doesn't have permission", function () {
  136. beforeEach(function (ctx) {
  137. ctx.AuthorizationManager.promises.canUserDeleteOrResolveThread
  138. .withArgs(ctx.userId, ctx.project_id, ctx.thread_id, ctx.token)
  139. .resolves(false)
  140. })
  141. invokeMiddleware('ensureUserCanDeleteOrResolveThread')
  142. expectForbidden()
  143. })
  144. })
  145. describe('ensureUserCanWriteProjectSettings', function () {
  146. describe('when renaming a project', function () {
  147. beforeEach(function (ctx) {
  148. ctx.req.body.name = 'new project name'
  149. })
  150. testMiddleware(
  151. 'ensureUserCanWriteProjectSettings',
  152. 'canUserRenameProject'
  153. )
  154. })
  155. describe('when setting another parameter', function () {
  156. beforeEach(function (ctx) {
  157. ctx.req.body.compiler = 'texlive-2017'
  158. })
  159. testMiddleware(
  160. 'ensureUserCanWriteProjectSettings',
  161. 'canUserWriteProjectSettings'
  162. )
  163. })
  164. })
  165. describe('ensureUserCanAdminProject', function () {
  166. testMiddleware('ensureUserCanAdminProject', 'canUserAdminProject')
  167. })
  168. describe('ensureUserIsSiteAdmin', function () {
  169. describe('with logged in user', function () {
  170. describe('when user has permission', function () {
  171. setupSiteAdmin(true)
  172. invokeMiddleware('ensureUserIsSiteAdmin')
  173. expectNext()
  174. })
  175. describe("when user doesn't have permission", function () {
  176. setupSiteAdmin(false)
  177. invokeMiddleware('ensureUserIsSiteAdmin')
  178. expectRedirectToRestricted()
  179. })
  180. })
  181. describe('with oauth user', function () {
  182. setupOAuthUser()
  183. describe('when user has permission', function () {
  184. setupSiteAdmin(true)
  185. invokeMiddleware('ensureUserIsSiteAdmin')
  186. expectNext()
  187. })
  188. describe("when user doesn't have permission", function () {
  189. setupSiteAdmin(false)
  190. invokeMiddleware('ensureUserIsSiteAdmin')
  191. expectRedirectToRestricted()
  192. })
  193. })
  194. describe('with anonymous user', function () {
  195. setupAnonymousUser()
  196. invokeMiddleware('ensureUserIsSiteAdmin')
  197. expectRedirectToRestricted()
  198. })
  199. })
  200. describe('blockRestrictedUserFromProject', function () {
  201. describe('for a restricted user', function () {
  202. setupPermission('isRestrictedUserForProject', true)
  203. invokeMiddleware('blockRestrictedUserFromProject')
  204. expectForbidden()
  205. })
  206. describe('for a regular user', function () {
  207. setupPermission('isRestrictedUserForProject', false)
  208. invokeMiddleware('blockRestrictedUserFromProject')
  209. expectNext()
  210. })
  211. })
  212. describe('ensureUserCanReadMultipleProjects', function () {
  213. beforeEach(function (ctx) {
  214. ctx.req.query = { project_ids: 'project1,project2' }
  215. })
  216. describe('with logged in user', function () {
  217. describe('when user has permission to access all projects', function () {
  218. beforeEach(function (ctx) {
  219. ctx.AuthorizationManager.promises.canUserReadProject
  220. .withArgs(ctx.userId, 'project1', ctx.token)
  221. .resolves(true)
  222. ctx.AuthorizationManager.promises.canUserReadProject
  223. .withArgs(ctx.userId, 'project2', ctx.token)
  224. .resolves(true)
  225. })
  226. invokeMiddleware('ensureUserCanReadMultipleProjects')
  227. expectNext()
  228. })
  229. describe("when user doesn't have permission to access one of the projects", function () {
  230. beforeEach(function (ctx) {
  231. ctx.AuthorizationManager.promises.canUserReadProject
  232. .withArgs(ctx.userId, 'project1', ctx.token)
  233. .resolves(true)
  234. ctx.AuthorizationManager.promises.canUserReadProject
  235. .withArgs(ctx.userId, 'project2', ctx.token)
  236. .resolves(false)
  237. })
  238. invokeMiddleware('ensureUserCanReadMultipleProjects')
  239. expectRedirectToRestricted()
  240. })
  241. })
  242. describe('with oauth user', function () {
  243. setupOAuthUser()
  244. beforeEach(function (ctx) {
  245. ctx.AuthorizationManager.promises.canUserReadProject
  246. .withArgs(ctx.userId, 'project1', ctx.token)
  247. .resolves(true)
  248. ctx.AuthorizationManager.promises.canUserReadProject
  249. .withArgs(ctx.userId, 'project2', ctx.token)
  250. .resolves(true)
  251. })
  252. invokeMiddleware('ensureUserCanReadMultipleProjects')
  253. expectNext()
  254. })
  255. describe('with anonymous user', function () {
  256. setupAnonymousUser()
  257. describe('when user has permission', function () {
  258. describe('when user has permission to access all projects', function () {
  259. beforeEach(function (ctx) {
  260. ctx.AuthorizationManager.promises.canUserReadProject
  261. .withArgs(null, 'project1', ctx.token)
  262. .resolves(true)
  263. ctx.AuthorizationManager.promises.canUserReadProject
  264. .withArgs(null, 'project2', ctx.token)
  265. .resolves(true)
  266. })
  267. invokeMiddleware('ensureUserCanReadMultipleProjects')
  268. expectNext()
  269. })
  270. describe("when user doesn't have permission to access one of the projects", function () {
  271. beforeEach(function (ctx) {
  272. ctx.AuthorizationManager.promises.canUserReadProject
  273. .withArgs(null, 'project1', ctx.token)
  274. .resolves(true)
  275. ctx.AuthorizationManager.promises.canUserReadProject
  276. .withArgs(null, 'project2', ctx.token)
  277. .resolves(false)
  278. })
  279. invokeMiddleware('ensureUserCanReadMultipleProjects')
  280. expectRedirectToRestricted()
  281. })
  282. })
  283. })
  284. })
  285. })
  286. function testMiddleware(middleware, permission) {
  287. describe(middleware, function () {
  288. describe('with missing project_id', function () {
  289. setupMissingProjectId()
  290. invokeMiddleware(middleware)
  291. expectError()
  292. })
  293. describe('with logged in user', function () {
  294. describe('when user has permission', function () {
  295. setupPermission(permission, true)
  296. invokeMiddleware(middleware)
  297. expectNext()
  298. })
  299. describe("when user doesn't have permission", function () {
  300. setupPermission(permission, false)
  301. invokeMiddleware(middleware)
  302. expectForbidden()
  303. })
  304. })
  305. describe('with oauth user', function () {
  306. setupOAuthUser()
  307. describe('when user has permission', function () {
  308. setupPermission(permission, true)
  309. invokeMiddleware(middleware)
  310. expectNext()
  311. })
  312. describe("when user doesn't have permission", function () {
  313. setupPermission(permission, false)
  314. invokeMiddleware(middleware)
  315. expectForbidden()
  316. })
  317. })
  318. describe('with anonymous user', function () {
  319. setupAnonymousUser()
  320. describe('when user has permission', function () {
  321. setupAnonymousPermission(permission, true)
  322. invokeMiddleware(middleware)
  323. expectNext()
  324. })
  325. describe("when user doesn't have permission", function () {
  326. setupAnonymousPermission(permission, false)
  327. invokeMiddleware(middleware)
  328. expectForbidden()
  329. })
  330. })
  331. describe('with malformed project id', function () {
  332. setupMalformedProjectId()
  333. invokeMiddleware(middleware)
  334. expectNotFound()
  335. })
  336. })
  337. }
  338. function setupAnonymousUser() {
  339. beforeEach(function (ctx) {
  340. ctx.SessionManager.getLoggedInUserId.returns(null)
  341. ctx.SessionManager.isUserLoggedIn.returns(false)
  342. })
  343. }
  344. function setupOAuthUser() {
  345. beforeEach(function (ctx) {
  346. ctx.SessionManager.getLoggedInUserId.returns(null)
  347. ctx.req.oauth_user = { _id: ctx.userId }
  348. })
  349. }
  350. function setupPermission(permission, value) {
  351. beforeEach(function (ctx) {
  352. ctx.AuthorizationManager.promises[permission]
  353. .withArgs(ctx.userId, ctx.project_id, ctx.token)
  354. .resolves(value)
  355. })
  356. }
  357. function setupAnonymousPermission(permission, value) {
  358. beforeEach(function (ctx) {
  359. ctx.AuthorizationManager.promises[permission]
  360. .withArgs(null, ctx.project_id, ctx.token)
  361. .resolves(value)
  362. })
  363. }
  364. function setupSiteAdmin(value) {
  365. beforeEach(function (ctx) {
  366. ctx.AuthorizationManager.promises.isUserSiteAdmin
  367. .withArgs(ctx.userId)
  368. .resolves(value)
  369. })
  370. }
  371. function setupMissingProjectId() {
  372. beforeEach(function (ctx) {
  373. delete ctx.req.params.project_id
  374. })
  375. }
  376. function setupMalformedProjectId() {
  377. beforeEach(function (ctx) {
  378. ctx.req.params = { project_id: 'bad-project-id' }
  379. })
  380. }
  381. function invokeMiddleware(method) {
  382. beforeEach(async function (ctx) {
  383. await new Promise(resolve => {
  384. ctx.next.callsFake(() => resolve())
  385. ctx.HttpErrorHandler.forbidden.callsFake(() => resolve())
  386. ctx.res.redirect.callsFake(() => resolve())
  387. ctx.AuthorizationMiddleware[method](ctx.req, ctx.res, ctx.next)
  388. })
  389. })
  390. }
  391. function expectNext() {
  392. it('calls the next middleware', function (ctx) {
  393. expect(ctx.next).to.have.been.calledWithExactly()
  394. })
  395. }
  396. function expectError() {
  397. it('calls the error middleware', function (ctx) {
  398. expect(ctx.next).to.have.been.calledWith(sinon.match.instanceOf(Error))
  399. })
  400. }
  401. function expectNotFound() {
  402. it('raises a 404', function (ctx) {
  403. expect(ctx.next).to.have.been.calledWith(
  404. sinon.match.instanceOf(Errors.NotFoundError)
  405. )
  406. })
  407. }
  408. function expectForbidden() {
  409. it('raises a 403', function (ctx) {
  410. expect(ctx.HttpErrorHandler.forbidden).to.have.been.calledWith(
  411. ctx.req,
  412. ctx.res
  413. )
  414. expect(ctx.next).not.to.have.been.called
  415. })
  416. }
  417. function expectRedirectToRestricted() {
  418. it('redirects to restricted', function (ctx) {
  419. expect(ctx.res.redirect).to.have.been.calledWith(
  420. '/restricted?from=%2Fcurrent%2Furl'
  421. )
  422. expect(ctx.next).not.to.have.been.called
  423. })
  424. }