Explorar el Código

Merge pull request #6614 from overleaf/jpa-msm-separate-admin-app

[misc] move admin capability from www. to admin. subdomain

GitOrigin-RevId: e0daeacf3c06b856ffb9fd35dce76e71f14e8459
Jakob Ackermann hace 4 años
padre
commit
e82a053c85
Se han modificado 25 ficheros con 223 adiciones y 20 borrados
  1. 1 0
      server-ce/Dockerfile
  2. 8 1
      services/web/app/src/Features/Authentication/AuthenticationController.js
  3. 5 3
      services/web/app/src/Features/Authorization/AuthorizationManager.js
  4. 1 2
      services/web/app/src/Features/Authorization/AuthorizationMiddleware.js
  5. 2 1
      services/web/app/src/Features/Collaborators/CollaboratorsController.js
  6. 11 0
      services/web/app/src/Features/Helpers/AdminAuthorizationHelper.js
  7. 2 1
      services/web/app/src/Features/Helpers/AuthorizationHelper.js
  8. 2 1
      services/web/app/src/Features/Project/ProjectController.js
  9. 2 1
      services/web/app/src/Features/Project/ProjectHelper.js
  10. 2 1
      services/web/app/src/Features/UserMembership/UserMembershipAuthorization.js
  11. 14 0
      services/web/app/src/infrastructure/Csrf.js
  12. 5 0
      services/web/app/src/infrastructure/ExpressLocals.js
  13. 9 4
      services/web/app/src/infrastructure/Server.js
  14. 1 1
      services/web/app/views/layout/navbar-marketing.pug
  15. 1 1
      services/web/app/views/layout/navbar.pug
  16. 6 0
      services/web/config/settings.defaults.js
  17. 1 0
      services/web/docker-compose.common.env
  18. 4 1
      services/web/modules/launchpad/app/src/LaunchpadController.js
  19. 3 1
      services/web/modules/launchpad/test/unit/src/LaunchpadControllerTests.js
  20. 64 0
      services/web/test/acceptance/src/AdminOnlyLoginTests.js
  21. 62 0
      services/web/test/acceptance/src/AdminPrivilegeAvailableTests.js
  22. 6 0
      services/web/test/unit/src/Authentication/AuthenticationControllerTests.js
  23. 4 1
      services/web/test/unit/src/Authorization/AuthorizationManagerTests.js
  24. 6 0
      services/web/test/unit/src/HelperFiles/AuthorizationHelperTests.js
  25. 1 0
      services/web/test/unit/src/Project/ProjectHelperTests.js

+ 1 - 0
server-ce/Dockerfile

@@ -67,6 +67,7 @@ RUN chmod +x /usr/local/bin/grunt
 ENV SHARELATEX_CONFIG /etc/sharelatex/settings.js
 
 ENV WEB_API_USER "sharelatex"
+ENV ADMIN_PRIVILEGE_AVAILABLE "true"
 
 ENV SHARELATEX_APP_NAME "Overleaf Community Edition"
 

+ 8 - 1
services/web/app/src/Features/Authentication/AuthenticationController.js

@@ -24,6 +24,7 @@ const {
   acceptsJson,
 } = require('../../infrastructure/RequestContentTypeDetection')
 const { ParallelLoginError } = require('./AuthenticationErrors')
+const { hasAdminAccess } = require('../Helpers/AdminAuthorizationHelper')
 
 function send401WithChallenge(res) {
   res.setHeader('WWW-Authenticate', 'OverleafLogin')
@@ -107,6 +108,12 @@ const AuthenticationController = {
       return res.redirect('/login')
     } // OAuth2 'state' mismatch
 
+    if (Settings.adminOnlyLogin && !hasAdminAccess(user)) {
+      return res.status(403).json({
+        message: { type: 'error', text: 'Admin only panel' },
+      })
+    }
+
     const auditInfo = AuthenticationController.getAuditInfo(req)
 
     const anonymousAnalyticsId = req.session.analyticsId
@@ -380,7 +387,7 @@ const AuthenticationController = {
       return next()
     }
     const user = SessionManager.getSessionUser(req.session)
-    if (!(user && user.isAdmin)) {
+    if (!hasAdminAccess(user)) {
       return next()
     }
     const email = user.email

+ 5 - 3
services/web/app/src/Features/Authorization/AuthorizationManager.js

@@ -8,6 +8,8 @@ const PrivilegeLevels = require('./PrivilegeLevels')
 const TokenAccessHandler = require('../TokenAccess/TokenAccessHandler')
 const PublicAccessLevels = require('./PublicAccessLevels')
 const Errors = require('../Errors/Errors')
+const { hasAdminAccess } = require('../Helpers/AdminAuthorizationHelper')
+const Settings = require('@overleaf/settings')
 
 function isRestrictedUser(userId, privilegeLevel, isTokenMember) {
   if (privilegeLevel === PrivilegeLevels.NONE) {
@@ -91,8 +93,7 @@ async function getPrivilegeLevelForProjectWithUser(
   }
 
   if (!opts.ignoreSiteAdmin) {
-    const isAdmin = await isUserSiteAdmin(userId)
-    if (isAdmin) {
+    if (await isUserSiteAdmin(userId)) {
       return PrivilegeLevels.OWNER
     }
   }
@@ -216,8 +217,9 @@ async function isUserSiteAdmin(userId) {
   if (!userId) {
     return false
   }
+  if (!Settings.adminPrivilegeAvailable) return false
   const user = await User.findOne({ _id: userId }, { isAdmin: 1 }).exec()
-  return user != null && user.isAdmin === true
+  return hasAdminAccess(user)
 }
 
 module.exports = {

+ 1 - 2
services/web/app/src/Features/Authorization/AuthorizationMiddleware.js

@@ -133,8 +133,7 @@ async function ensureUserCanAdminProject(req, res, next) {
 
 async function ensureUserIsSiteAdmin(req, res, next) {
   const userId = _getUserId(req)
-  const isAdmin = await AuthorizationManager.promises.isUserSiteAdmin(userId)
-  if (isAdmin) {
+  if (await AuthorizationManager.promises.isUserSiteAdmin(userId)) {
     logger.log({ userId }, 'allowing user admin access to site')
     return next()
   }

+ 2 - 1
services/web/app/src/Features/Collaborators/CollaboratorsController.js

@@ -10,6 +10,7 @@ const TagsHandler = require('../Tags/TagsHandler')
 const Errors = require('../Errors/Errors')
 const logger = require('@overleaf/logger')
 const { expressify } = require('../../util/promises')
+const { hasAdminAccess } = require('../Helpers/AdminAuthorizationHelper')
 
 module.exports = {
   removeUserFromProject: expressify(removeUserFromProject),
@@ -82,7 +83,7 @@ async function transferOwnership(req, res, next) {
       projectId,
       toUserId,
       {
-        allowTransferToNonCollaborators: sessionUser.isAdmin,
+        allowTransferToNonCollaborators: hasAdminAccess(sessionUser),
         sessionUserId: ObjectId(sessionUser._id),
       }
     )

+ 11 - 0
services/web/app/src/Features/Helpers/AdminAuthorizationHelper.js

@@ -0,0 +1,11 @@
+const Settings = require('@overleaf/settings')
+
+module.exports = {
+  hasAdminAccess,
+}
+
+function hasAdminAccess(user) {
+  if (!Settings.adminPrivilegeAvailable) return false
+  if (!user) return false
+  return Boolean(user.isAdmin)
+}

+ 2 - 1
services/web/app/src/Features/Helpers/AuthorizationHelper.js

@@ -1,11 +1,12 @@
 const { UserSchema } = require('../../models/User')
+const { hasAdminAccess } = require('./AdminAuthorizationHelper')
 
 module.exports = {
   hasAnyStaffAccess,
 }
 
 function hasAnyStaffAccess(user) {
-  if (user.isAdmin) {
+  if (hasAdminAccess(user)) {
     return true
   }
   if (!user.staffAccess) {

+ 2 - 1
services/web/app/src/Features/Project/ProjectController.js

@@ -39,6 +39,7 @@ const SplitTestHandler = require('../SplitTests/SplitTestHandler')
 const FeaturesUpdater = require('../Subscription/FeaturesUpdater')
 const SpellingHandler = require('../Spelling/SpellingHandler')
 const UserPrimaryEmailCheckHandler = require('../User/UserPrimaryEmailCheckHandler')
+const { hasAdminAccess } = require('../Helpers/AdminAuthorizationHelper')
 
 const _ssoAvailable = (affiliation, session, linkedInstitutionIds) => {
   if (!affiliation.institution) return false
@@ -965,7 +966,7 @@ const ProjectController = {
                 refProviders: _.mapValues(user.refProviders, Boolean),
                 alphaProgram: user.alphaProgram,
                 betaProgram: user.betaProgram,
-                isAdmin: user.isAdmin,
+                isAdmin: hasAdminAccess(user),
               },
               userSettings: {
                 mode: user.ace.mode,

+ 2 - 1
services/web/app/src/Features/Project/ProjectHelper.js

@@ -2,6 +2,7 @@ const { ObjectId } = require('mongodb')
 const _ = require('lodash')
 const { promisify } = require('util')
 const Settings = require('@overleaf/settings')
+const { hasAdminAccess } = require('../Helpers/AdminAuthorizationHelper')
 
 const ENGINE_TO_COMPILER_MAP = {
   latex_dvipdf: 'latex',
@@ -165,7 +166,7 @@ function _addNumericSuffixToProjectName(name, allProjectNames, maxLength) {
 
 function getAllowedImagesForUser(sessionUser) {
   const images = Settings.allowedImageNames || []
-  if (sessionUser && sessionUser.isAdmin) {
+  if (hasAdminAccess(sessionUser)) {
     return images
   } else {
     return images.filter(image => !image.adminOnly)

+ 2 - 1
services/web/app/src/Features/UserMembership/UserMembershipAuthorization.js

@@ -1,10 +1,11 @@
+const { hasAdminAccess } = require('../Helpers/AdminAuthorizationHelper')
 const UserMembershipAuthorization = {
   hasStaffAccess(requiredStaffAccess) {
     return req => {
       if (!req.user) {
         return false
       }
-      if (req.user.isAdmin) {
+      if (hasAdminAccess(req.user)) {
         return true
       }
       return (

+ 14 - 0
services/web/app/src/infrastructure/Csrf.js

@@ -15,6 +15,8 @@
 const csurf = require('csurf')
 const csrf = csurf()
 const { promisify } = require('util')
+const Settings = require('@overleaf/settings')
+const logger = require('@overleaf/logger')
 
 // Wrapper for `csurf` middleware that provides a list of routes that can be excluded from csrf checks.
 //
@@ -35,6 +37,18 @@ class Csrf {
     this.excluded_routes = {}
   }
 
+  static blockCrossOriginRequests() {
+    return function (req, res, next) {
+      const { origin } = req.headers
+      // NOTE: Only cross-origin requests must have an origin header set.
+      if (origin && !Settings.allowedOrigins.includes(origin)) {
+        logger.warn({ req }, 'blocking cross-origin request')
+        return res.sendStatus(403)
+      }
+      next()
+    }
+  }
+
   disableDefaultCsrfProtection(route, method) {
     if (!this.excluded_routes[route]) {
       this.excluded_routes[route] = {}

+ 5 - 0
services/web/app/src/infrastructure/ExpressLocals.js

@@ -12,6 +12,9 @@ const SessionManager = require('../Features/Authentication/SessionManager')
 const PackageVersions = require('./PackageVersions')
 const Modules = require('./Modules')
 const SafeHTMLSubstitute = require('../Features/Helpers/SafeHTMLSubstitution')
+const {
+  hasAdminAccess,
+} = require('../Features/Helpers/AdminAuthorizationHelper')
 
 let webpackManifest
 switch (process.env.NODE_ENV) {
@@ -299,6 +302,8 @@ module.exports = function (webRouter, privateApiRouter, publicApiRouter) {
     res.locals.getLoggedInUserId = () =>
       SessionManager.getLoggedInUserId(req.session)
     res.locals.getSessionUser = () => SessionManager.getSessionUser(req.session)
+    res.locals.hasAdminAccess = () =>
+      hasAdminAccess(SessionManager.getSessionUser(req.session))
     next()
   })
 

+ 9 - 4
services/web/app/src/infrastructure/Server.js

@@ -40,6 +40,9 @@ const HttpErrorHandler = require('../Features/Errors/HttpErrorHandler')
 const UserSessionsManager = require('../Features/User/UserSessionsManager')
 const AuthenticationController = require('../Features/Authentication/AuthenticationController')
 const SessionManager = require('../Features/Authentication/SessionManager')
+const {
+  hasAdminAccess,
+} = require('../Features/Helpers/AdminAuthorizationHelper')
 
 const STATIC_CACHE_AGE = Settings.cacheStaticAssets
   ? oneDayInMilliseconds * 365
@@ -141,6 +144,11 @@ app.use(methodOverride())
 app.use(bearerToken())
 
 app.use(metrics.http.monitor(logger))
+
+if (Settings.blockCrossOriginRequests) {
+  app.use(Csrf.blockCrossOriginRequests())
+}
+
 RedirectManager.apply(webRouter)
 ProxyManager.apply(publicApiRouter)
 
@@ -228,10 +236,7 @@ webRouter.use(SessionAutostartMiddleware.invokeCallbackMiddleware)
 webRouter.use(function (req, res, next) {
   if (Settings.siteIsOpen) {
     next()
-  } else if (
-    SessionManager.getSessionUser(req.session) &&
-    SessionManager.getSessionUser(req.session).isAdmin
-  ) {
+  } else if (hasAdminAccess(SessionManager.getSessionUser(req.session))) {
     next()
   } else {
     HttpErrorHandler.maintenance(req, res)

+ 1 - 1
services/web/app/views/layout/navbar-marketing.pug

@@ -15,7 +15,7 @@ nav.navbar.navbar-default.navbar-main
 			else
 				a(href='/', aria-label=settings.appName).navbar-brand
 
-		- var canDisplayAdminMenu = getSessionUser() && getSessionUser().isAdmin
+		- var canDisplayAdminMenu = hasAdminAccess()
 		- var canDisplaySplitTestMenu = hasFeature('saas') && (canDisplayAdminMenu || (getSessionUser() && getSessionUser().staffAccess && (getSessionUser().staffAccess.splitTestMetrics || getSessionUser().staffAccess.splitTestManagement)))
 		.navbar-collapse.collapse(data-ol-navbar-main-collapse)
 			ul.nav.navbar-nav.navbar-right

+ 1 - 1
services/web/app/views/layout/navbar.pug

@@ -10,7 +10,7 @@ nav.navbar.navbar-default.navbar-main
 			else
 				a(href='/', aria-label=settings.appName).navbar-brand
 
-		- var canDisplayAdminMenu = getSessionUser() && getSessionUser().isAdmin
+		- var canDisplayAdminMenu = hasAdminAccess()
 		- var canDisplaySplitTestMenu = hasFeature('saas') && (canDisplayAdminMenu || (getSessionUser() && getSessionUser().staffAccess && (getSessionUser().staffAccess.splitTestMetrics || getSessionUser().staffAccess.splitTestManagement)))
 		.navbar-collapse.collapse(collapse="navCollapsed")
 			ul.nav.navbar-nav.navbar-right

+ 6 - 0
services/web/config/settings.defaults.js

@@ -283,6 +283,12 @@ module.exports = {
     bcryptRounds: parseInt(process.env.BCRYPT_ROUNDS, 10) || 12,
   }, // number of rounds used to hash user passwords (raised to power 2)
 
+  adminUrl: process.env.ADMIN_URL,
+  adminOnlyLogin: process.env.ADMIN_ONLY_LOGIN === 'true',
+  adminPrivilegeAvailable: process.env.ADMIN_PRIVILEGE_AVAILABLE === 'true',
+  blockCrossOriginRequests: process.env.BLOCK_CROSS_ORIGIN_REQUESTS === 'true',
+  allowedOrigins: (process.env.ALLOWED_ORIGINS || siteUrl).split(','),
+
   httpAuthUsers,
 
   // Default features

+ 1 - 0
services/web/docker-compose.common.env

@@ -1,3 +1,4 @@
+ADMIN_PRIVILEGE_AVAILABLE=true
 BCRYPT_ROUNDS=1
 REDIS_HOST=redis
 QUEUES_REDIS_HOST=redis

+ 4 - 1
services/web/modules/launchpad/app/src/LaunchpadController.js

@@ -25,6 +25,9 @@ const UserGetter = require('../../../../app/src/Features/User/UserGetter')
 const { User } = require('../../../../app/src/models/User')
 const AuthenticationController = require('../../../../app/src/Features/Authentication/AuthenticationController')
 const SessionManager = require('../../../../app/src/Features/Authentication/SessionManager')
+const {
+  hasAdminAccess,
+} = require('../../../../app/src/Features/Helpers/AdminAuthorizationHelper')
 
 module.exports = LaunchpadController = {
   _getAuthMethod() {
@@ -67,7 +70,7 @@ module.exports = LaunchpadController = {
             if (err != null) {
               return next(err)
             }
-            if (user && user.isAdmin) {
+            if (hasAdminAccess(user)) {
               return res.render(Path.resolve(__dirname, '../views/launchpad'), {
                 wsUrl: Settings.wsUrl,
                 adminUserExists,

+ 3 - 1
services/web/modules/launchpad/test/unit/src/LaunchpadControllerTests.js

@@ -31,7 +31,9 @@ describe('LaunchpadController', function () {
     this.User = {}
     this.LaunchpadController = SandboxedModule.require(modulePath, {
       requires: {
-        '@overleaf/settings': (this.Settings = {}),
+        '@overleaf/settings': (this.Settings = {
+          adminPrivilegeAvailable: true,
+        }),
         '@overleaf/metrics': (this.Metrics = {}),
         '../../../../app/src/Features/User/UserRegistrationHandler':
           (this.UserRegistrationHandler = {}),

+ 64 - 0
services/web/test/acceptance/src/AdminOnlyLoginTests.js

@@ -0,0 +1,64 @@
+const Settings = require('@overleaf/settings')
+const { expect } = require('chai')
+const User = require('./helpers/User').promises
+
+describe('AdminOnlyLogin', function () {
+  let adminUser, regularUser
+  const flagBefore = Settings.adminOnlyLogin
+  after(function () {
+    Settings.adminOnlyLogin = flagBefore
+  })
+
+  beforeEach('create admin user', async function () {
+    adminUser = new User()
+    await adminUser.ensureUserExists()
+    await adminUser.ensureAdmin()
+  })
+
+  beforeEach('create regular user', async function () {
+    regularUser = new User()
+    await regularUser.ensureUserExists()
+  })
+
+  async function expectCanLogin(user) {
+    const response = await user.login()
+    expect(response.statusCode).to.equal(200)
+    expect(response.body).to.deep.equal({ redir: '/project' })
+  }
+
+  async function expectRejectedLogin(user) {
+    const response = await user.login()
+    expect(response.statusCode).to.equal(403)
+    expect(response.body).to.deep.equal({
+      message: { type: 'error', text: 'Admin only panel' },
+    })
+  }
+
+  describe('adminOnlyLogin=true', function () {
+    beforeEach(function () {
+      Settings.adminOnlyLogin = true
+    })
+
+    it('should allow the admin user to login', async function () {
+      await expectCanLogin(adminUser)
+    })
+
+    it('should block a regular user from login', async function () {
+      await expectRejectedLogin(regularUser)
+    })
+  })
+
+  describe('adminOnlyLogin=false', function () {
+    beforeEach(function () {
+      Settings.adminOnlyLogin = false
+    })
+
+    it('should allow the admin user to login', async function () {
+      await expectCanLogin(adminUser)
+    })
+
+    it('should allow a regular user to login', async function () {
+      await expectCanLogin(regularUser)
+    })
+  })
+})

+ 62 - 0
services/web/test/acceptance/src/AdminPrivilegeAvailableTests.js

@@ -0,0 +1,62 @@
+const Settings = require('@overleaf/settings')
+const { expect } = require('chai')
+const User = require('./helpers/User').promises
+
+describe('AdminPrivilegeAvailable', function () {
+  let adminUser
+  const flagBefore = Settings.adminPrivilegeAvailable
+  after(function () {
+    Settings.adminPrivilegeAvailable = flagBefore
+  })
+
+  beforeEach('create admin user', async function () {
+    adminUser = new User()
+    await adminUser.ensureUserExists()
+    await adminUser.ensureAdmin()
+    await adminUser.login()
+  })
+
+  let projectIdOwned, otherUsersProjectId
+  beforeEach('create owned project', async function () {
+    projectIdOwned = await adminUser.createProject('owned project')
+  })
+
+  beforeEach('create other user and project', async function () {
+    const otherUser = new User()
+    await otherUser.login()
+
+    otherUsersProjectId = await otherUser.createProject('other users project')
+  })
+
+  async function hasAccess(projectId) {
+    const { response } = await adminUser.doRequest(
+      'GET',
+      `/project/${projectId}`
+    )
+    return response.statusCode === 200
+  }
+
+  describe('adminPrivilegeAvailable=true', function () {
+    beforeEach(function () {
+      Settings.adminPrivilegeAvailable = true
+    })
+    it('should grant the admin access to owned project', async function () {
+      expect(await hasAccess(projectIdOwned)).to.equal(true)
+    })
+    it('should grant the admin access to non-owned project', async function () {
+      expect(await hasAccess(otherUsersProjectId)).to.equal(true)
+    })
+  })
+
+  describe('adminPrivilegeAvailable=false', function () {
+    beforeEach(function () {
+      Settings.adminPrivilegeAvailable = false
+    })
+    it('should grant the admin access to owned project', async function () {
+      expect(await hasAccess(projectIdOwned)).to.equal(true)
+    })
+    it('should block the admin from non-owned project', async function () {
+      expect(await hasAccess(otherUsersProjectId)).to.equal(false)
+    })
+  })
+})

+ 6 - 0
services/web/test/unit/src/Authentication/AuthenticationControllerTests.js

@@ -33,6 +33,10 @@ describe('AuthenticationController', function () {
 
     this.AuthenticationController = SandboxedModule.require(modulePath, {
       requires: {
+        '../Helpers/AdminAuthorizationHelper': (this.AdminAuthorizationHelper =
+          {
+            hasAdminAccess: sinon.stub().returns(false),
+          }),
         './AuthenticationErrors': AuthenticationErrors,
         '../User/UserAuditLogHandler': (this.UserAuditLogHandler = {
           addEntry: sinon.stub().yields(null),
@@ -158,6 +162,7 @@ describe('AuthenticationController', function () {
       this.SessionManager.getSessionUser = sinon
         .stub()
         .returns({ isAdmin: true })
+      this.AdminAuthorizationHelper.hasAdminAccess.returns(true)
       this.AuthenticationController.validateAdmin(this.req, this.res, err => {
         this.SessionManager.getSessionUser.called.should.equal(true)
         expect(err).to.exist
@@ -167,6 +172,7 @@ describe('AuthenticationController', function () {
 
     it('should block an admin with a bad domain', function (done) {
       this.SessionManager.getSessionUser = sinon.stub().returns(this.badAdmin)
+      this.AdminAuthorizationHelper.hasAdminAccess.returns(true)
       this.AuthenticationController.validateAdmin(this.req, this.res, err => {
         this.SessionManager.getSessionUser.called.should.equal(true)
         expect(err).to.exist

+ 4 - 1
services/web/test/unit/src/Authorization/AuthorizationManagerTests.js

@@ -54,7 +54,10 @@ describe('AuthorizationManager', function () {
         '../Project/ProjectGetter': this.ProjectGetter,
         '../../models/User': { User: this.User },
         '../TokenAccess/TokenAccessHandler': this.TokenAccessHandler,
-        '@overleaf/settings': { passwordStrengthOptions: {} },
+        '@overleaf/settings': {
+          passwordStrengthOptions: {},
+          adminPrivilegeAvailable: true,
+        },
       },
     })
   })

+ 6 - 0
services/web/test/unit/src/HelperFiles/AuthorizationHelperTests.js

@@ -1,4 +1,5 @@
 const SandboxedModule = require('sandboxed-module')
+const sinon = require('sinon')
 const { expect } = require('chai')
 const modulePath = '../../../../app/src/Features/Helpers/AuthorizationHelper'
 
@@ -6,6 +7,9 @@ describe('AuthorizationHelper', function () {
   beforeEach(function () {
     this.AuthorizationHelper = SandboxedModule.require(modulePath, {
       requires: {
+        './AdminAuthorizationHelper': (this.AdminAuthorizationHelper = {
+          hasAdminAccess: sinon.stub().returns(false),
+        }),
         '../../models/User': {
           UserSchema: {
             obj: {
@@ -38,11 +42,13 @@ describe('AuthorizationHelper', function () {
 
     it('with admin user', function () {
       const user = { isAdmin: true }
+      this.AdminAuthorizationHelper.hasAdminAccess.returns(true)
       expect(this.AuthorizationHelper.hasAnyStaffAccess(user)).to.be.true
     })
 
     it('with staff user', function () {
       const user = { staffAccess: { adminMetrics: true, somethingElse: false } }
+      this.AdminAuthorizationHelper.hasAdminAccess.returns(true)
       expect(this.AuthorizationHelper.hasAnyStaffAccess(user)).to.be.true
     })
 

+ 1 - 0
services/web/test/unit/src/Project/ProjectHelperTests.js

@@ -22,6 +22,7 @@ describe('ProjectHelper', function () {
     }
 
     this.Settings = {
+      adminPrivilegeAvailable: true,
       allowedImageNames: [
         { imageName: 'texlive-full:2018.1', imageDesc: 'TeX Live 2018' },
         { imageName: 'texlive-full:2019.1', imageDesc: 'TeX Live 2019' },