diff --git a/services/web/app/src/Features/Helpers/AdminAuthorizationHelper.js b/services/web/app/src/Features/Helpers/AdminAuthorizationHelper.js index 6a86e53847..216921e58b 100644 --- a/services/web/app/src/Features/Helpers/AdminAuthorizationHelper.js +++ b/services/web/app/src/Features/Helpers/AdminAuthorizationHelper.js @@ -1,8 +1,10 @@ const Settings = require('@overleaf/settings') +const Modules = require('../../infrastructure/Modules') module.exports = { hasAdminAccess, canRedirectToAdminDomain, + getAdminCapabilities, } function hasAdminAccess(user) { @@ -11,6 +13,18 @@ function hasAdminAccess(user) { return Boolean(user.isAdmin) } +async function getAdminCapabilities(user) { + const rawAdminCapabilties = await Modules.promises.hooks.fire( + 'getAdminCapabilities', + user + ) + + return { + adminCapabilities: [...new Set(rawAdminCapabilties.flat())], + adminCapabilitiesAvailable: rawAdminCapabilties.length > 0, + } +} + function canRedirectToAdminDomain(user) { if (Settings.adminPrivilegeAvailable) return false if (!Settings.adminUrl) return false diff --git a/services/web/app/src/infrastructure/ExpressLocals.js b/services/web/app/src/infrastructure/ExpressLocals.js index 76564ff5d4..0032b2c682 100644 --- a/services/web/app/src/infrastructure/ExpressLocals.js +++ b/services/web/app/src/infrastructure/ExpressLocals.js @@ -14,12 +14,14 @@ const Modules = require('./Modules') const Errors = require('../Features/Errors/Errors') const { canRedirectToAdminDomain, + getAdminCapabilities, hasAdminAccess, } = require('../Features/Helpers/AdminAuthorizationHelper') const { addOptionalCleanupHandlerAfterDrainingConnections, } = require('./GracefulShutdown') const { sanitizeSessionUserForFrontEnd } = require('./FrontEndUser') +const { expressify } = require('@overleaf/promise-utils') const IEEE_BRAND_ID = Settings.ieeeBrandId @@ -312,6 +314,35 @@ module.exports = function (webRouter, privateApiRouter, publicApiRouter) { next() }) + webRouter.use( + expressify(async function (req, res, next) { + const user = SessionManager.getSessionUser(req.session) + try { + const { adminCapabilities, adminCapabilitiesAvailable } = + await getAdminCapabilities(user) + res.locals.hasAdminCapability = capability => { + if (!hasAdminAccess(user)) { + return false + } + if (!adminCapabilitiesAvailable) { + // If admin capabilities are not available, then all admins have all capabilities + return true + } + return adminCapabilities.includes(capability) + } + } catch (error) { + if (user) { + // This is unexpected, it probably means that the session user does not exist. + logger.warn({ error, req, user }, 'Failed to get admin capabilities') + } + // adminCapabilitiesAvailable should be true if we are here so deny to be safe + res.locals.hasAdminCapability = () => false + } + + next() + }) + ) + webRouter.use(function (req, res, next) { // Clone the nav settings so they can be modified for each request res.locals.nav = {} diff --git a/services/web/test/unit/src/HelperFiles/AdminAuthorizationHelperTests.js b/services/web/test/unit/src/HelperFiles/AdminAuthorizationHelperTests.js new file mode 100644 index 0000000000..ba4b532f8f --- /dev/null +++ b/services/web/test/unit/src/HelperFiles/AdminAuthorizationHelperTests.js @@ -0,0 +1,60 @@ +const { expect } = require('chai') +const SandboxedModule = require('sandboxed-module') +const sinon = require('sinon') + +const modulePath = + '../../../../app/src/Features/Helpers/AdminAuthorizationHelper' + +describe('AdminAuthorizationHelper', function () { + beforeEach(function () { + this.fireHook = sinon.stub().resolves([]) + this.AdminAuthorizationHelper = SandboxedModule.require(modulePath, { + requires: { + '@overleaf/settings': { + adminPrivilegeAvailable: true, + adminUrl: 'https://admin.overleaf.com', + }, + '../../infrastructure/Modules': { + promises: { + hooks: { + fire: this.fireHook, + }, + }, + }, + }, + }) + }) + describe('getAdminCapabilities', function () { + describe('when modules return capabilities', function () { + let result + const module1Capabilities = ['capability1', 'capability2'] + const module2Capabilities = ['capability2', 'capability3'] + + beforeEach(async function () { + this.fireHook.resolves([module1Capabilities, module2Capabilities]) + result = await this.AdminAuthorizationHelper.getAdminCapabilities({}) + }) + it('returns true for adminCapabilitiesAvailable', async function () { + expect(result.adminCapabilitiesAvailable).to.be.true + }) + it('returns a flattened array of the returned capabilities', function () { + expect(result.adminCapabilities) + .to.be.an('array') + .that.includes(...module1Capabilities, ...module2Capabilities) + }) + }) + describe('when no module returns capabilities', function () { + let result + beforeEach(async function () { + result = await this.AdminAuthorizationHelper.getAdminCapabilities({}) + }) + + it('returns false for adminCapabilitiesAvailable', function () { + expect(result.adminCapabilitiesAvailable).to.be.false + }) + it('returns an empty adminCapabilities array', function () { + expect(result.adminCapabilities).to.be.an('array').that.is.empty + }) + }) + }) +})