Files
overleaf-cep/services/web/test/unit/src/Authorization/AuthorizationMiddleware.test.mjs
Antoine Clausse 71f0b28a84 [web] Convert some Features files to ES modules (part 3) (#28494)
* Rename files to mjs

* Rename test files to mjs

* Update CODEOWNERS

* Update files to ESM

* Update test files to ESM

* Update RestoreManager.test.mjs

* Remove unused `AdminAuthorizationHelper` mock and stub

* Remove unnecessary return

GitOrigin-RevId: 2b9ef126de1d8964afbc6e5641cca36712655866
2025-09-17 08:05:02 +00:00

494 lines
14 KiB
JavaScript

import { vi, expect } from 'vitest'
import sinon from 'sinon'
import mongodb from 'mongodb-legacy'
import Errors from '../../../../app/src/Features/Errors/Errors.js'
const { ObjectId } = mongodb
const MODULE_PATH =
'../../../../app/src/Features/Authorization/AuthorizationMiddleware.mjs'
describe('AuthorizationMiddleware', function () {
beforeEach(async function (ctx) {
ctx.userId = new ObjectId().toString()
ctx.project_id = new ObjectId().toString()
ctx.doc_id = new ObjectId().toString()
ctx.thread_id = new ObjectId().toString()
ctx.token = 'some-token'
ctx.AuthenticationController = {}
ctx.SessionManager = {
getSessionUser: sinon.stub().returns(null),
getLoggedInUserId: sinon.stub().returns(ctx.userId),
isUserLoggedIn: sinon.stub().returns(true),
}
ctx.AuthorizationManager = {
promises: {
canUserReadProject: sinon.stub(),
canUserWriteProjectSettings: sinon.stub(),
canUserWriteProjectContent: sinon.stub(),
canUserWriteOrReviewProjectContent: sinon.stub(),
canUserDeleteOrResolveThread: sinon.stub(),
canUserAdminProject: sinon.stub(),
canUserRenameProject: sinon.stub(),
isUserSiteAdmin: sinon.stub(),
isRestrictedUserForProject: sinon.stub(),
},
}
ctx.HttpErrorHandler = {
forbidden: sinon.stub(),
}
ctx.TokenAccessHandler = {
getRequestToken: sinon.stub().returns(ctx.token),
}
ctx.DocumentUpdaterHandler = {
promises: {
getComment: sinon.stub().resolves(),
},
}
vi.doMock('../../../../app/src/Features/Errors/Errors.js', () => ({
default: Errors,
}))
vi.doMock(
'../../../../app/src/Features/Authorization/AuthorizationManager',
() => ({
default: ctx.AuthorizationManager,
})
)
vi.doMock('../../../../app/src/Features/Errors/HttpErrorHandler', () => ({
default: ctx.HttpErrorHandler,
}))
vi.doMock('mongodb-legacy', () => ({
default: { ObjectId },
}))
vi.doMock(
'../../../../app/src/Features/Authentication/AuthenticationController',
() => ({
default: ctx.AuthenticationController,
})
)
vi.doMock(
'../../../../app/src/Features/Authentication/SessionManager',
() => ({
default: ctx.SessionManager,
})
)
vi.doMock(
'../../../../app/src/Features/TokenAccess/TokenAccessHandler',
() => ({
default: ctx.TokenAccessHandler,
})
)
vi.doMock(
'../../../../app/src/Features/Helpers/AdminAuthorizationHelper',
() => ({
default: {
canRedirectToAdminDomain: sinon.stub().returns(false),
},
})
)
vi.doMock(
'../../../../app/src/Features/DocumentUpdater/DocumentUpdaterHandler',
() => ({
default: ctx.DocumentUpdaterHandler,
})
)
ctx.AuthorizationMiddleware = (await import(MODULE_PATH)).default
ctx.req = {
params: {
project_id: ctx.project_id,
},
body: {},
}
ctx.res = {
redirect: sinon.stub(),
locals: {
currentUrl: '/current/url',
},
}
ctx.next = sinon.stub()
})
describe('ensureCanReadProject', function () {
testMiddleware('ensureUserCanReadProject', 'canUserReadProject')
})
describe('ensureUserCanWriteProjectContent', function () {
testMiddleware(
'ensureUserCanWriteProjectContent',
'canUserWriteProjectContent'
)
})
describe('ensureUserCanWriteOrReviewProjectContent', function () {
testMiddleware(
'ensureUserCanWriteOrReviewProjectContent',
'canUserWriteOrReviewProjectContent'
)
})
describe('ensureUserCanDeleteOrResolveThread', function () {
beforeEach(function (ctx) {
ctx.req.params.thread_id = ctx.thread_id
})
describe('when user has permission', function () {
beforeEach(function (ctx) {
ctx.AuthorizationManager.promises.canUserDeleteOrResolveThread
.withArgs(ctx.userId, ctx.project_id, ctx.thread_id, ctx.token)
.resolves(true)
})
invokeMiddleware('ensureUserCanDeleteOrResolveThread')
expectNext()
})
describe("when user doesn't have permission", function () {
beforeEach(function (ctx) {
ctx.AuthorizationManager.promises.canUserDeleteOrResolveThread
.withArgs(ctx.userId, ctx.project_id, ctx.thread_id, ctx.token)
.resolves(false)
})
invokeMiddleware('ensureUserCanDeleteOrResolveThread')
expectForbidden()
})
})
describe('ensureUserCanWriteProjectSettings', function () {
describe('when renaming a project', function () {
beforeEach(function (ctx) {
ctx.req.body.name = 'new project name'
})
testMiddleware(
'ensureUserCanWriteProjectSettings',
'canUserRenameProject'
)
})
describe('when setting another parameter', function () {
beforeEach(function (ctx) {
ctx.req.body.compiler = 'texlive-2017'
})
testMiddleware(
'ensureUserCanWriteProjectSettings',
'canUserWriteProjectSettings'
)
})
})
describe('ensureUserCanAdminProject', function () {
testMiddleware('ensureUserCanAdminProject', 'canUserAdminProject')
})
describe('ensureUserIsSiteAdmin', function () {
describe('with logged in user', function () {
describe('when user has permission', function () {
setupSiteAdmin(true)
invokeMiddleware('ensureUserIsSiteAdmin')
expectNext()
})
describe("when user doesn't have permission", function () {
setupSiteAdmin(false)
invokeMiddleware('ensureUserIsSiteAdmin')
expectRedirectToRestricted()
})
})
describe('with oauth user', function () {
setupOAuthUser()
describe('when user has permission', function () {
setupSiteAdmin(true)
invokeMiddleware('ensureUserIsSiteAdmin')
expectNext()
})
describe("when user doesn't have permission", function () {
setupSiteAdmin(false)
invokeMiddleware('ensureUserIsSiteAdmin')
expectRedirectToRestricted()
})
})
describe('with anonymous user', function () {
setupAnonymousUser()
invokeMiddleware('ensureUserIsSiteAdmin')
expectRedirectToRestricted()
})
})
describe('blockRestrictedUserFromProject', function () {
describe('for a restricted user', function () {
setupPermission('isRestrictedUserForProject', true)
invokeMiddleware('blockRestrictedUserFromProject')
expectForbidden()
})
describe('for a regular user', function () {
setupPermission('isRestrictedUserForProject', false)
invokeMiddleware('blockRestrictedUserFromProject')
expectNext()
})
})
describe('ensureUserCanReadMultipleProjects', function () {
beforeEach(function (ctx) {
ctx.req.query = { project_ids: 'project1,project2' }
})
describe('with logged in user', function () {
describe('when user has permission to access all projects', function () {
beforeEach(function (ctx) {
ctx.AuthorizationManager.promises.canUserReadProject
.withArgs(ctx.userId, 'project1', ctx.token)
.resolves(true)
ctx.AuthorizationManager.promises.canUserReadProject
.withArgs(ctx.userId, 'project2', ctx.token)
.resolves(true)
})
invokeMiddleware('ensureUserCanReadMultipleProjects')
expectNext()
})
describe("when user doesn't have permission to access one of the projects", function () {
beforeEach(function (ctx) {
ctx.AuthorizationManager.promises.canUserReadProject
.withArgs(ctx.userId, 'project1', ctx.token)
.resolves(true)
ctx.AuthorizationManager.promises.canUserReadProject
.withArgs(ctx.userId, 'project2', ctx.token)
.resolves(false)
})
invokeMiddleware('ensureUserCanReadMultipleProjects')
expectRedirectToRestricted()
})
})
describe('with oauth user', function () {
setupOAuthUser()
beforeEach(function (ctx) {
ctx.AuthorizationManager.promises.canUserReadProject
.withArgs(ctx.userId, 'project1', ctx.token)
.resolves(true)
ctx.AuthorizationManager.promises.canUserReadProject
.withArgs(ctx.userId, 'project2', ctx.token)
.resolves(true)
})
invokeMiddleware('ensureUserCanReadMultipleProjects')
expectNext()
})
describe('with anonymous user', function () {
setupAnonymousUser()
describe('when user has permission', function () {
describe('when user has permission to access all projects', function () {
beforeEach(function (ctx) {
ctx.AuthorizationManager.promises.canUserReadProject
.withArgs(null, 'project1', ctx.token)
.resolves(true)
ctx.AuthorizationManager.promises.canUserReadProject
.withArgs(null, 'project2', ctx.token)
.resolves(true)
})
invokeMiddleware('ensureUserCanReadMultipleProjects')
expectNext()
})
describe("when user doesn't have permission to access one of the projects", function () {
beforeEach(function (ctx) {
ctx.AuthorizationManager.promises.canUserReadProject
.withArgs(null, 'project1', ctx.token)
.resolves(true)
ctx.AuthorizationManager.promises.canUserReadProject
.withArgs(null, 'project2', ctx.token)
.resolves(false)
})
invokeMiddleware('ensureUserCanReadMultipleProjects')
expectRedirectToRestricted()
})
})
})
})
})
function testMiddleware(middleware, permission) {
describe(middleware, function () {
describe('with missing project_id', function () {
setupMissingProjectId()
invokeMiddleware(middleware)
expectError()
})
describe('with logged in user', function () {
describe('when user has permission', function () {
setupPermission(permission, true)
invokeMiddleware(middleware)
expectNext()
})
describe("when user doesn't have permission", function () {
setupPermission(permission, false)
invokeMiddleware(middleware)
expectForbidden()
})
})
describe('with oauth user', function () {
setupOAuthUser()
describe('when user has permission', function () {
setupPermission(permission, true)
invokeMiddleware(middleware)
expectNext()
})
describe("when user doesn't have permission", function () {
setupPermission(permission, false)
invokeMiddleware(middleware)
expectForbidden()
})
})
describe('with anonymous user', function () {
setupAnonymousUser()
describe('when user has permission', function () {
setupAnonymousPermission(permission, true)
invokeMiddleware(middleware)
expectNext()
})
describe("when user doesn't have permission", function () {
setupAnonymousPermission(permission, false)
invokeMiddleware(middleware)
expectForbidden()
})
})
describe('with malformed project id', function () {
setupMalformedProjectId()
invokeMiddleware(middleware)
expectNotFound()
})
})
}
function setupAnonymousUser() {
beforeEach(function (ctx) {
ctx.SessionManager.getLoggedInUserId.returns(null)
ctx.SessionManager.isUserLoggedIn.returns(false)
})
}
function setupOAuthUser() {
beforeEach(function (ctx) {
ctx.SessionManager.getLoggedInUserId.returns(null)
ctx.req.oauth_user = { _id: ctx.userId }
})
}
function setupPermission(permission, value) {
beforeEach(function (ctx) {
ctx.AuthorizationManager.promises[permission]
.withArgs(ctx.userId, ctx.project_id, ctx.token)
.resolves(value)
})
}
function setupAnonymousPermission(permission, value) {
beforeEach(function (ctx) {
ctx.AuthorizationManager.promises[permission]
.withArgs(null, ctx.project_id, ctx.token)
.resolves(value)
})
}
function setupSiteAdmin(value) {
beforeEach(function (ctx) {
ctx.AuthorizationManager.promises.isUserSiteAdmin
.withArgs(ctx.userId)
.resolves(value)
})
}
function setupMissingProjectId() {
beforeEach(function (ctx) {
delete ctx.req.params.project_id
})
}
function setupMalformedProjectId() {
beforeEach(function (ctx) {
ctx.req.params = { project_id: 'bad-project-id' }
})
}
function invokeMiddleware(method) {
beforeEach(async function (ctx) {
await new Promise(resolve => {
ctx.next.callsFake(() => resolve())
ctx.HttpErrorHandler.forbidden.callsFake(() => resolve())
ctx.res.redirect.callsFake(() => resolve())
ctx.AuthorizationMiddleware[method](ctx.req, ctx.res, ctx.next)
})
})
}
function expectNext() {
it('calls the next middleware', function (ctx) {
expect(ctx.next).to.have.been.calledWithExactly()
})
}
function expectError() {
it('calls the error middleware', function (ctx) {
expect(ctx.next).to.have.been.calledWith(sinon.match.instanceOf(Error))
})
}
function expectNotFound() {
it('raises a 404', function (ctx) {
expect(ctx.next).to.have.been.calledWith(
sinon.match.instanceOf(Errors.NotFoundError)
)
})
}
function expectForbidden() {
it('raises a 403', function (ctx) {
expect(ctx.HttpErrorHandler.forbidden).to.have.been.calledWith(
ctx.req,
ctx.res
)
expect(ctx.next).not.to.have.been.called
})
}
function expectRedirectToRestricted() {
it('redirects to restricted', function (ctx) {
expect(ctx.res.redirect).to.have.been.calledWith(
'/restricted?from=%2Fcurrent%2Furl'
)
expect(ctx.next).not.to.have.been.called
})
}