Merge pull request #31327 from overleaf/ar-allow-split-test-ui-without-admin-privilege

[web/admin-roles] allow split test UI without admin privilege

GitOrigin-RevId: 1d10153d7762196dd7a8df835af6193b38670fbc
This commit is contained in:
Andrew Rumble
2026-04-01 08:06:25 +00:00
committed by Copybot
parent 5513f9d378
commit 972c6e4710
7 changed files with 66 additions and 10 deletions
@@ -11,6 +11,9 @@ export default {
getAdminCapabilities,
useHasAdminCapability,
useAdminCapabilities: expressify(useAdminCapabilities),
useNonAdminDomainCapabilities: expressify(useNonAdminDomainCapabilities),
hasNonAdminDomainCapability,
useHasNonAdminDomainCapability,
}
function hasAdminAccess(user) {
@@ -36,6 +39,36 @@ function hasAdminCapability(capability, requireAdminRoles = true) {
}
}
function hasNonAdminDomainCapability(capability) {
return req => {
return req.nonAdminDomainCapabilities?.includes(capability)
}
}
async function useNonAdminDomainCapabilities(req, _res, next) {
if (req.nonAdminDomainCapabilities) {
return next()
}
const user = SessionManager.getSessionUser(req.session)
// Equivalent to `hasAdminAccess` but without the `Settings.adminPrivilegeAvailable` check.
if (!user?.isAdmin) {
req.nonAdminDomainCapabilities = []
return next()
}
try {
const capabilities = await Modules.promises.hooks.fire(
'getNonAdminDomainCapabilities',
user
)
req.nonAdminDomainCapabilities = [...new Set(capabilities.flat())]
} catch (err) {
// This can throw when the user doesn't exist or isn't logged in.
req.nonAdminDomainCapabilities = []
}
next()
}
async function getAdminCapabilities(user) {
const rawAdminCapabilties = await Modules.promises.hooks.fire(
'getAdminCapabilities',
@@ -77,6 +110,12 @@ function useHasAdminCapability(req, res, next) {
next()
}
function useHasNonAdminDomainCapability(req, res, next) {
res.locals.hasNonAdminDomainCapability = capability =>
hasNonAdminDomainCapability(capability)(req)
next()
}
function canRedirectToAdminDomain(user) {
if (Settings.adminPrivilegeAvailable) return false
if (!Settings.adminUrl) return false
@@ -3,6 +3,8 @@ import SessionManager from '../Authentication/SessionManager.mjs'
const UserMembershipAuthorization = {
hasAdminCapability: AdminAuthorizationHelper.hasAdminCapability,
hasNonAdminDomainCapability:
AdminAuthorizationHelper.hasNonAdminDomainCapability,
hasAdminAccess(req) {
return AdminAuthorizationHelper.hasAdminAccess(
@@ -12,7 +12,8 @@ import TemplatesManager from '../Templates/TemplatesManager.mjs'
import { z, zz, parseReq } from '../../infrastructure/Validation.mjs'
import AdminAuthorizationHelper from '../Helpers/AdminAuthorizationHelper.mjs'
const { useAdminCapabilities } = AdminAuthorizationHelper
const { useAdminCapabilities, useNonAdminDomainCapabilities } =
AdminAuthorizationHelper
// set of middleware arrays or functions that checks user access to an entity
// (publisher, institution, group, template, etc.)
const UserMembershipMiddleware = {
@@ -209,17 +210,21 @@ const UserMembershipMiddleware = {
requireSplitTestMetricsAccess: [
AuthenticationController.requireLogin(),
useAdminCapabilities,
useNonAdminDomainCapabilities,
allowAccessIfAny([
UserMembershipAuthorization.hasAdminCapability('view-split-test'),
UserMembershipAuthorization.hasNonAdminDomainCapability(
'view-split-test'
),
]),
],
requireSplitTestManagementAccess: [
AuthenticationController.requireLogin(),
useAdminCapabilities,
useNonAdminDomainCapabilities,
allowAccessIfAny([
UserMembershipAuthorization.hasAdminCapability('modify-split-test'),
UserMembershipAuthorization.hasNonAdminDomainCapability(
'modify-split-test'
),
]),
],
@@ -21,6 +21,8 @@ const {
hasAdminAccess,
useAdminCapabilities,
useHasAdminCapability,
useNonAdminDomainCapabilities,
useHasNonAdminDomainCapability,
} = AdminAuthorizationHelper
const IEEE_BRAND_ID = Settings.ieeeBrandId
@@ -279,8 +281,9 @@ export default async function (webRouter, privateApiRouter, publicApiRouter) {
})
webRouter.use(useAdminCapabilities)
webRouter.use(useHasAdminCapability)
webRouter.use(useNonAdminDomainCapabilities)
webRouter.use(useHasNonAdminDomainCapability)
webRouter.use(function (req, res, next) {
// Clone the nav settings so they can be modified for each request
+1 -1
View File
@@ -12,7 +12,7 @@ block append meta
- const canDisplayAdminRedirect = canRedirectToAdminDomain()
- const sessionUser = getSessionUser()
- const canDisplayProjectUrlLookup = settings.adminPrivilegeAvailable && canDisplayAdminMenu && hasAdminCapability('view-project-setting', false)
- const canDisplaySplitTestMenu = hasFeature('saas') && canDisplayAdminMenu && hasAdminCapability('view-split-test')
- const canDisplaySplitTestMenu = hasFeature('saas') && hasNonAdminDomainCapability('view-split-test')
- const canDisplaySurveyMenu = hasFeature('saas') && canDisplayAdminMenu && hasAdminCapability('manage-survey', false)
- const canDisplayScriptLogMenu = hasFeature('saas') && hasAdminCapability('view-script-log', false) && canDisplayAdminMenu
- const enableUpgradeButton = projectDashboardReact && usersBestSubscription && (usersBestSubscription.type === 'free' || usersBestSubscription.type === 'standalone-ai-add-on')
@@ -31,7 +31,7 @@ nav.navbar.navbar-default.navbar-main.navbar-expand-lg(
- var canDisplayAdminMenu = hasAdminAccess()
- var canDisplayAdminRedirect = canRedirectToAdminDomain()
- var canDisplayProjectUrlLookup = settings.adminPrivilegeAvailable && canDisplayAdminMenu && hasAdminCapability('view-project-setting', false)
- var canDisplaySplitTestMenu = hasFeature('saas') && canDisplayAdminMenu && hasAdminCapability('view-split-test')
- var canDisplaySplitTestMenu = hasFeature('saas') && hasNonAdminDomainCapability('view-split-test')
- var canDisplaySurveyMenu = hasFeature('saas') && canDisplayAdminMenu && hasAdminCapability('manage-survey', false)
- var canDisplayScriptLogMenu = hasFeature('saas') && hasAdminCapability('view-script-log', false) && canDisplayAdminMenu
+9 -2
View File
@@ -28,13 +28,20 @@ async function createUser(email) {
const features = email.startsWith('free+')
? Settings.defaultFeatures
: Settings.features.professional
const isAdmin = email.startsWith('admin+')
let adminRoles = []
if (email.startsWith('admin+finance')) {
adminRoles = ['finance']
} else if (isAdmin) {
adminRoles = ['engineering']
}
await db.users.updateOne(
{ _id: user._id },
{
$set: {
// Set admin flag.
isAdmin: email.startsWith('admin+'),
adminRoles: email.startsWith('admin+') ? ['engineering'] : [],
isAdmin,
adminRoles,
// Disable spell-checking for performance and flakiness reasons.
'ace.spellCheckLanguage': '',
// Override features.