Explorar o código

[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
Antoine Clausse hai 1 ano
pai
achega
86e74b9228

+ 55 - 21
services/web/app/src/Features/Authorization/AuthorizationManager.js

@@ -156,10 +156,10 @@ async function getPrivilegeLevelForProjectWithUser(
       return PrivilegeLevels.OWNER
       return PrivilegeLevels.OWNER
     }
     }
     const { adminCapabilities } = await getAdminCapabilities({ _id: userId })
     const { adminCapabilities } = await getAdminCapabilities({ _id: userId })
-    if (adminCapabilities.includes('modify-project')) {
+    if (adminCapabilities.includes('modify-project-content')) {
       return PrivilegeLevels.OWNER
       return PrivilegeLevels.OWNER
     }
     }
-    if (adminCapabilities.includes('view-project')) {
+    if (adminCapabilities.includes('view-project-content')) {
       adminReadOnly = true
       adminReadOnly = true
     }
     }
   }
   }
@@ -261,24 +261,31 @@ async function canUserReadProject(userId, projectId, token) {
   const privilegeLevel = await getPrivilegeLevelForProject(
   const privilegeLevel = await getPrivilegeLevelForProject(
     userId,
     userId,
     projectId,
     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) {
 async function canUserWriteProjectContent(userId, projectId, token) {
   const privilegeLevel = await getPrivilegeLevelForProject(
   const privilegeLevel = await getPrivilegeLevelForProject(
     userId,
     userId,
     projectId,
     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(
   const privilegeLevel = await getPrivilegeLevelForProject(
     userId,
     userId,
     projectId,
     projectId,
-    token
+    token,
+    { ignoreSiteAdmin: true }
   )
   )
   return (
   return (
     privilegeLevel === PrivilegeLevels.OWNER ||
     privilegeLevel === PrivilegeLevels.OWNER ||
     privilegeLevel === PrivilegeLevels.READ_AND_WRITE ||
     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,
     userId,
     projectId,
     projectId,
     token,
     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(
   const privilegeLevel = await getPrivilegeLevelForProject(
     userId,
     userId,
     projectId,
     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) {
 async function canUserAdminProject(userId, projectId, token) {
   const privilegeLevel = await getPrivilegeLevelForProject(
   const privilegeLevel = await getPrivilegeLevelForProject(
     userId,
     userId,
     projectId,
     projectId,
-    token
+    token,
+    { ignoreSiteAdmin: true }
+  )
+  return (
+    privilegeLevel === PrivilegeLevels.OWNER ||
+    (await hasAdminProjectCapability(userId, 'modify-project-setting'))
   )
   )
-  return privilegeLevel === PrivilegeLevels.OWNER
 }
 }
 
 
 async function isUserSiteAdmin(userId) {
 async function isUserSiteAdmin(userId) {
@@ -334,6 +353,21 @@ async function isUserSiteAdmin(userId) {
   return hasAdminAccess(user)
   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(
 async function canUserDeleteOrResolveThread(
   userId,
   userId,
   projectId,
   projectId,

+ 81 - 0
services/web/test/acceptance/src/AuthorizationTests.mjs

@@ -364,6 +364,87 @@ describe('Authorization', function () {
         await expectRedirectedAdminAccess(this.site_admin)
         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 () {
   describe('shared project', function () {

+ 10 - 3
services/web/test/unit/src/Authorization/AuthorizationManagerTests.js

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

+ 4 - 2
services/web/types/admin-capabilities.ts

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