[web] Update admin permissions to view/modify project contents (#28162)

* Split capability definitions `modify-project`/`view-project` into `modify-project-content`/`modify-project-setting`/`view-project-content`/`view-project-setting`

* Add admin capabilities check in AuthorizationManager

* Update checks in router

* Update frontend checks

* Remove UI elements for admins without `view-project-content`

* Update tests

* Remove `modify-project-content` from the roles' capabilities

* Update tests

* Add "with admin roles" tests in AuthorizationTests.mjs

GitOrigin-RevId: 3311bcb2da792968927b5b3703b24e069d0baf5b
This commit is contained in:
Antoine Clausse
2025-09-04 11:36:42 +02:00
committed by Copybot
parent 408a55460a
commit 86e74b9228
4 changed files with 150 additions and 26 deletions

View File

@@ -156,10 +156,10 @@ async function getPrivilegeLevelForProjectWithUser(
return PrivilegeLevels.OWNER
}
const { adminCapabilities } = await getAdminCapabilities({ _id: userId })
if (adminCapabilities.includes('modify-project')) {
if (adminCapabilities.includes('modify-project-content')) {
return PrivilegeLevels.OWNER
}
if (adminCapabilities.includes('view-project')) {
if (adminCapabilities.includes('view-project-content')) {
adminReadOnly = true
}
}
@@ -261,24 +261,31 @@ async function canUserReadProject(userId, projectId, token) {
const privilegeLevel = await getPrivilegeLevelForProject(
userId,
projectId,
token
token,
{ ignoreSiteAdmin: true }
)
return (
[
PrivilegeLevels.OWNER,
PrivilegeLevels.READ_AND_WRITE,
PrivilegeLevels.READ_ONLY,
PrivilegeLevels.REVIEW,
].includes(privilegeLevel) ||
(await hasAdminProjectCapability(userId, 'view-project-content'))
)
return [
PrivilegeLevels.OWNER,
PrivilegeLevels.READ_AND_WRITE,
PrivilegeLevels.READ_ONLY,
PrivilegeLevels.REVIEW,
].includes(privilegeLevel)
}
async function canUserWriteProjectContent(userId, projectId, token) {
const privilegeLevel = await getPrivilegeLevelForProject(
userId,
projectId,
token
token,
{ ignoreSiteAdmin: true }
)
return [PrivilegeLevels.OWNER, PrivilegeLevels.READ_AND_WRITE].includes(
privilegeLevel
return (
[PrivilegeLevels.OWNER, PrivilegeLevels.READ_AND_WRITE].includes(
privilegeLevel
) || (await hasAdminProjectCapability(userId, 'modify-project-content'))
)
}
@@ -286,12 +293,14 @@ async function canUserWriteOrReviewProjectContent(userId, projectId, token) {
const privilegeLevel = await getPrivilegeLevelForProject(
userId,
projectId,
token
token,
{ ignoreSiteAdmin: true }
)
return (
privilegeLevel === PrivilegeLevels.OWNER ||
privilegeLevel === PrivilegeLevels.READ_AND_WRITE ||
privilegeLevel === PrivilegeLevels.REVIEW
privilegeLevel === PrivilegeLevels.REVIEW ||
(await hasAdminProjectCapability(userId, 'modify-project-content'))
)
}
@@ -300,10 +309,12 @@ async function canUserWriteProjectSettings(userId, projectId, token) {
userId,
projectId,
token,
{ ignorePublicAccess: true }
{ ignorePublicAccess: true, ignoreSiteAdmin: true }
)
return [PrivilegeLevels.OWNER, PrivilegeLevels.READ_AND_WRITE].includes(
privilegeLevel
return (
[PrivilegeLevels.OWNER, PrivilegeLevels.READ_AND_WRITE].includes(
privilegeLevel
) || (await hasAdminProjectCapability(userId, 'modify-project-setting'))
)
}
@@ -311,18 +322,26 @@ async function canUserRenameProject(userId, projectId, token) {
const privilegeLevel = await getPrivilegeLevelForProject(
userId,
projectId,
token
token,
{ ignoreSiteAdmin: true }
)
return (
privilegeLevel === PrivilegeLevels.OWNER ||
(await hasAdminProjectCapability(userId, 'modify-project-setting'))
)
return privilegeLevel === PrivilegeLevels.OWNER
}
async function canUserAdminProject(userId, projectId, token) {
const privilegeLevel = await getPrivilegeLevelForProject(
userId,
projectId,
token
token,
{ ignoreSiteAdmin: true }
)
return (
privilegeLevel === PrivilegeLevels.OWNER ||
(await hasAdminProjectCapability(userId, 'modify-project-setting'))
)
return privilegeLevel === PrivilegeLevels.OWNER
}
async function isUserSiteAdmin(userId) {
@@ -334,6 +353,21 @@ async function isUserSiteAdmin(userId) {
return hasAdminAccess(user)
}
/**
* @param {string} userId
* @param {'view-project-setting'|'view-project-content'|'modify-project-setting'|'modify-project-content'} adminCapability
*/
async function hasAdminProjectCapability(userId, adminCapability) {
if (!Settings.adminPrivilegeAvailable || !(await isUserSiteAdmin(userId))) {
return false
}
if (!Settings.adminRolesEnabled) {
return true
}
const { adminCapabilities } = await getAdminCapabilities({ _id: userId })
return adminCapabilities.includes(adminCapability)
}
async function canUserDeleteOrResolveThread(
userId,
projectId,

View File

@@ -364,6 +364,87 @@ describe('Authorization', function () {
await expectRedirectedAdminAccess(this.site_admin)
})
})
describe('with admin roles', function () {
beforeEach(function () {
if (!settings.moduleImportSequence.includes('admin-roles')) {
this.skip()
}
settings.adminRolesEnabled = true
settings.adminPrivilegeAvailable = true
})
afterEach(function () {
settings.adminRolesEnabled = false
settings.adminPrivilegeAvailable = true
this.site_admin.mongoUpdate({
$set: { adminRoles: [] },
})
})
describe('engineering', function () {
beforeEach(function () {
this.site_admin.mongoUpdate({
$set: { adminRoles: ['engineering'] },
})
})
it('should allow site admin users read access to it', async function () {
await expectReadAccess(this.site_admin, this.projectId)
})
it('should not allow site admin users write access to its content', async function () {
await expectNoContentWriteAccess(this.site_admin, this.projectId)
})
it('should allow site admin users write access to its settings', async function () {
await expectSettingsWriteAccess(this.site_admin, this.projectId)
})
it('should allow site admin users to rename the project', async function () {
await expectRenameProjectAccess(this.site_admin, this.projectId)
})
it('should allow site admin users project admin access to it', async function () {
await expectProjectAdminAccess(this.site_admin, this.projectId)
})
it('should allow site admin users site admin access to site admin endpoints', async function () {
await expectAdminAccess(this.site_admin)
})
})
describe('no admin role assigned', function () {
beforeEach(function () {
this.site_admin.mongoUpdate({
$set: { adminRoles: [] },
})
})
it('should not allow site admin users read access to it', async function () {
await expectNoReadAccess(this.site_admin, this.projectId)
})
it('should not allow site admin users write access to its content', async function () {
await expectNoContentWriteAccess(this.site_admin, this.projectId)
})
it('should not allow site admin users write access to its settings', async function () {
await expectNoSettingsWriteAccess(this.site_admin, this.projectId)
})
it('should not allow site admin users to rename the project', async function () {
await expectNoRenameProjectAccess(this.site_admin, this.projectId)
})
it('should not allow site admin users project admin access to it', async function () {
await expectNoProjectAdminAccess(this.site_admin, this.projectId)
})
it('should allow site admin users site admin access to site admin endpoints', async function () {
await expectAdminAccess(this.site_admin)
})
})
})
})
describe('shared project', function () {

View File

@@ -709,13 +709,13 @@ function testPermission(permission, privilegeLevels) {
})
expectPermission(permission, false)
})
describe('admin with `view-project`', function () {
describe('admin with `view-project-content`', function () {
beforeEach(function () {
this.user.isAdmin = true
this.settings.adminRolesEnabled = true
this.Modules.promises.hooks.fire
.withArgs('getAdminCapabilities')
.resolves([['view-project']])
.resolves([['view-project-content']])
})
expectPermission(permission, privilegeLevels.readOnly || false)
})
@@ -725,7 +725,14 @@ function testPermission(permission, privilegeLevels) {
this.settings.adminRolesEnabled = true
this.Modules.promises.hooks.fire
.withArgs('getAdminCapabilities')
.resolves([['view-project', 'modify-project']])
.resolves([
[
'view-project-content',
'view-project-setting',
'modify-project-content',
'modify-project-setting',
],
])
})
expectPermission(permission, privilegeLevels.siteAdmin || false)
})

View File

@@ -11,14 +11,16 @@ export type AdminCapability =
| 'modify-managed-group'
| 'modify-managed-group-member'
| 'modify-user-account-status'
| 'modify-project'
| 'modify-project-content'
| 'modify-project-setting'
| 'manage-survey'
| 'modify-split-test'
| 'modify-user-email'
| 'modify-user-name'
| 'view-audit-log'
| 'view-group-manager'
| 'view-project'
| 'view-project-content'
| 'view-project-setting'
| 'view-session'
| 'view-script-log'
| 'view-split-test'