فهرست منبع

[web] Add admin permission `modify-group-manager` (#27642)

* Add capacity `modify-group-manager`

* Check `modify-group-manager` (backend)

* Check `modify-group-manager` (frontend)

* Update tests

* Rename AdminPermissions to mjs

* Add `ol-adminCapabilities` in frontend tests

* Allow modifying group managers if `adminRolesEnabled` is false

* Add `adminPrivilegeAvailable` check

* Update: set `ol-canModify` boolean instead of `ol-adminCapabilities`

* Mock `hasAnyAccess`

* Use `hasAdminCapability` helper

* Add `ol-canModify` to types

* Remove `isAdminMiddleware` as we don't want to relax the permissions for now

* Fix: pass `res` to `hasAnyAccess` (!!)

* * Check `hasWriteAccess` (`hasAdminCapability('modify-group-manager')` or `staffAccess.groupManagement`) in the Pug file
* Fix: Check `hasWriteAccess` in the publisher and institution pug files (!)
* Revert `hasAnyAccess` changes
* Rename `ol-canModify` to `ol-hasWriteAccess` for consistency with other variables

* Remove redundant file AdminPermissions.mjs

* Update unit test

* Revert changes to UserMembershipController.test.mjs

* Rename to `requireGroupManagersWriteAccess`

GitOrigin-RevId: f3f0b1b17abd1d2f0c363688e87d9063de886e3c
Antoine Clausse 11 ماه پیش
والد
کامیت
af44f478b9

+ 1 - 0
services/web/app/src/Features/UserMembership/UserMembershipController.mjs

@@ -121,6 +121,7 @@ async function _renderManagersPage(req, res, next, template) {
     name: entityName,
     users,
     groupId: entityPrimaryKey,
+    entityAccess: UserMembershipAuthorization.hasEntityAccess()(req),
   })
 }
 

+ 13 - 0
services/web/app/src/Features/UserMembership/UserMembershipMiddleware.js

@@ -86,6 +86,19 @@ const UserMembershipMiddleware = {
     ]),
   ],
 
+  requireGroupManagersWriteAccess: [
+    AuthenticationController.requireLogin(),
+    fetchEntityConfig('groupManagers'),
+    fetchEntity(),
+    requireEntity(),
+    useAdminCapabilities,
+    allowAccessIfAny([
+      UserMembershipAuthorization.hasEntityAccess(),
+      UserMembershipAuthorization.hasStaffAccess('groupManagement'),
+      UserMembershipAuthorization.hasAdminCapability('modify-group-manager'),
+    ]),
+  ],
+
   requireGroupAdminAccess: [
     AuthenticationController.requireLogin(),
     fetchEntityConfig('groupAdmin'),

+ 2 - 2
services/web/app/src/Features/UserMembership/UserMembershipRouter.mjs

@@ -61,12 +61,12 @@ export default {
     )
     webRouter.post(
       '/manage/groups/:id/managers',
-      UserMembershipMiddleware.requireGroupManagersManagementAccess,
+      UserMembershipMiddleware.requireGroupManagersWriteAccess,
       UserMembershipController.add
     )
     webRouter.delete(
       '/manage/groups/:id/managers/:userId',
-      UserMembershipMiddleware.requireGroupManagersManagementAccess,
+      UserMembershipMiddleware.requireGroupManagersWriteAccess,
       UserMembershipController.remove
     )
 

+ 2 - 0
services/web/app/views/user_membership/group-managers-react.pug

@@ -4,10 +4,12 @@ block entrypointVar
 	- entrypoint = 'pages/user/subscription/group-management/group-managers'
 
 block append meta
+	- var hasWriteAccess = entityAccess || (hasAdminAccess() && hasAdminCapability('modify-group-manager')) || (getSessionUser().staffAccess && getSessionUser().staffAccess.groupManagement)
 	meta(name='ol-user' data-type='json' content=user)
 	meta(name='ol-users' data-type='json' content=users)
 	meta(name='ol-groupId' data-type='string' content=groupId)
 	meta(name='ol-groupName' data-type='string' content=name)
+	meta(name='ol-hasWriteAccess' data-type='boolean' content=hasWriteAccess)
 
 block content
 	main#subscription-manage-group-root.content.content-alt

+ 1 - 0
services/web/app/views/user_membership/institution-managers-react.pug

@@ -8,6 +8,7 @@ block append meta
 	meta(name='ol-users' data-type='json' content=users)
 	meta(name='ol-groupId' data-type='string' content=groupId)
 	meta(name='ol-groupName' data-type='string' content=name)
+	meta(name='ol-hasWriteAccess' data-type='boolean' content)
 
 block content
 	main#subscription-manage-group-root.content.content-alt

+ 1 - 0
services/web/app/views/user_membership/publisher-managers-react.pug

@@ -8,6 +8,7 @@ block append meta
 	meta(name='ol-users' data-type='json' content=users)
 	meta(name='ol-groupId' data-type='string' content=groupId)
 	meta(name='ol-groupName' data-type='string' content=name)
+	meta(name='ol-hasWriteAccess' data-type='boolean' content)
 
 block content
 	main#subscription-manage-group-root.content.content-alt

+ 49 - 41
services/web/frontend/js/features/group-management/components/managers-table.tsx

@@ -56,6 +56,7 @@ export function ManagersTable({
   const [inviteError, setInviteError] = useState<APIError>()
   const [removeMemberInflightCount, setRemoveMemberInflightCount] = useState(0)
   const [removeMemberError, setRemoveMemberError] = useState<APIError>()
+  const hasWriteAccess = getMeta('ol-hasWriteAccess')
 
   const addManagers = useCallback(
     (e: FormEvent | React.MouseEvent) => {
@@ -181,13 +182,15 @@ export function ManagersTable({
                 <thead>
                   <tr>
                     <th className="cell-checkbox">
-                      <OLFormCheckbox
-                        autoComplete="off"
-                        onChange={handleSelectAllClick}
-                        checked={selectedUsers.length === users.length}
-                        aria-label={t('select_all')}
-                        data-testid="select-all-checkbox"
-                      />
+                      {hasWriteAccess && (
+                        <OLFormCheckbox
+                          autoComplete="off"
+                          onChange={handleSelectAllClick}
+                          checked={selectedUsers.length === users.length}
+                          aria-label={t('select_all')}
+                          data-testid="select-all-checkbox"
+                        />
+                      )}
                     </th>
                     <th>{t('email')}</th>
                     <th className="cell-name">{t('name')}</th>
@@ -225,45 +228,50 @@ export function ManagersTable({
                       selectUser={selectUser}
                       unselectUser={unselectUser}
                       selected={selectedUsers.includes(user)}
+                      hasWriteAccess={hasWriteAccess}
                     />
                   ))}
                 </tbody>
               </OLTable>
             </div>
-            <hr />
-            <div>
-              <p className="small">{t('add_more_managers')}</p>
-              <ErrorAlert error={inviteError} />
-              <form onSubmit={addManagers} data-testid="add-members-form">
-                <OLRow>
-                  <OLCol xs={6}>
-                    <OLFormControl
-                      type="input"
-                      placeholder="jane@example.com, joe@example.com"
-                      aria-describedby="add-members-description"
-                      value={emailString}
-                      onChange={handleEmailsChange}
-                    />
-                  </OLCol>
-                  <OLCol xs={4}>
-                    <OLButton
-                      variant="primary"
-                      onClick={addManagers}
-                      isLoading={inviteUserInflightCount > 0}
-                    >
-                      {t('add')}
-                    </OLButton>
-                  </OLCol>
-                </OLRow>
-                <OLRow>
-                  <OLCol xs={8}>
-                    <OLFormText>
-                      {t('add_comma_separated_emails_help')}
-                    </OLFormText>
-                  </OLCol>
-                </OLRow>
-              </form>
-            </div>
+            {hasWriteAccess && (
+              <>
+                <hr />
+                <div>
+                  <p className="small">{t('add_more_managers')}</p>
+                  <ErrorAlert error={inviteError} />
+                  <form onSubmit={addManagers} data-testid="add-members-form">
+                    <OLRow>
+                      <OLCol xs={6}>
+                        <OLFormControl
+                          type="input"
+                          placeholder="jane@example.com, joe@example.com"
+                          aria-describedby="add-members-description"
+                          value={emailString}
+                          onChange={handleEmailsChange}
+                        />
+                      </OLCol>
+                      <OLCol xs={4}>
+                        <OLButton
+                          variant="primary"
+                          onClick={addManagers}
+                          isLoading={inviteUserInflightCount > 0}
+                        >
+                          {t('add')}
+                        </OLButton>
+                      </OLCol>
+                    </OLRow>
+                    <OLRow>
+                      <OLCol xs={8}>
+                        <OLFormText>
+                          {t('add_comma_separated_emails_help')}
+                        </OLFormText>
+                      </OLCol>
+                    </OLRow>
+                  </form>
+                </div>
+              </>
+            )}
           </OLCard>
         </OLCol>
       </OLRow>

+ 11 - 7
services/web/frontend/js/features/group-management/components/user-row.tsx

@@ -10,6 +10,7 @@ type GroupMemberRowProps = {
   selectUser: (user: User) => void
   unselectUser: (user: User) => void
   selected: boolean
+  hasWriteAccess: boolean
 }
 
 export default function UserRow({
@@ -17,6 +18,7 @@ export default function UserRow({
   selectUser,
   unselectUser,
   selected,
+  hasWriteAccess,
 }: GroupMemberRowProps) {
   const { t } = useTranslation()
 
@@ -34,13 +36,15 @@ export default function UserRow({
   return (
     <tr key={`user-${user.email}`} className="managed-entity-row">
       <td className="cell-checkbox">
-        <OLFormCheckbox
-          autoComplete="off"
-          checked={selected}
-          onChange={e => handleSelectUser(e, user)}
-          aria-label={t('select_user')}
-          data-testid="select-single-checkbox"
-        />
+        {hasWriteAccess && (
+          <OLFormCheckbox
+            autoComplete="off"
+            checked={selected}
+            onChange={e => handleSelectUser(e, user)}
+            aria-label={t('select_user')}
+            data-testid="select-single-checkbox"
+          />
+        )}
       </td>
       <td>{user.email}</td>
       <td className="cell-name">

+ 1 - 0
services/web/test/frontend/features/group-management/components/group-managers.spec.tsx

@@ -28,6 +28,7 @@ describe('group managers', function () {
       win.metaAttributesCache.set('ol-users', [JOHN_DOE, BOBBY_LAPOINTE])
       win.metaAttributesCache.set('ol-groupId', GROUP_ID)
       win.metaAttributesCache.set('ol-groupName', 'My Awesome Team')
+      win.metaAttributesCache.set('ol-hasWriteAccess', true)
     })
 
     cy.mount(<GroupManagers />)

+ 1 - 0
services/web/test/frontend/features/group-management/components/institution-managers.spec.tsx

@@ -28,6 +28,7 @@ describe('institution managers', function () {
       win.metaAttributesCache.set('ol-users', [JOHN_DOE, BOBBY_LAPOINTE])
       win.metaAttributesCache.set('ol-groupId', GROUP_ID)
       win.metaAttributesCache.set('ol-groupName', 'My Awesome Institution')
+      win.metaAttributesCache.set('ol-hasWriteAccess', true)
     })
 
     cy.mount(<InstitutionManagers />)

+ 1 - 0
services/web/test/frontend/features/group-management/components/publisher-managers.spec.tsx

@@ -28,6 +28,7 @@ describe('publisher managers', function () {
       win.metaAttributesCache.set('ol-users', [JOHN_DOE, BOBBY_LAPOINTE])
       win.metaAttributesCache.set('ol-groupId', GROUP_ID)
       win.metaAttributesCache.set('ol-groupName', 'My Awesome Publisher')
+      win.metaAttributesCache.set('ol-hasWriteAccess', true)
     })
 
     cy.mount(<PublisherManagers />)

+ 5 - 0
services/web/test/unit/src/UserMembership/UserMembershipController.test.mjs

@@ -9,6 +9,7 @@ import {
   UserNotFoundError,
   UserAlreadyAddedError,
 } from '../../../../app/src/Features/UserMembership/UserMembershipErrors.js'
+
 const assertCalledWith = sinon.assert.calledWith
 
 const modulePath =
@@ -46,6 +47,7 @@ describe('UserMembershipController', function () {
         institution.name = 'Test Institution Name'
         callback(null, institution)
       },
+      managerIds: ['mock-member-id-1'],
     }
     ctx.users = [
       {
@@ -205,6 +207,7 @@ describe('UserMembershipController', function () {
       ctx.req.user = ctx.user
       ctx.req.entity = ctx.subscription
       ctx.req.entityConfig = EntityConfigs.group
+      ctx.Modules.promises.hooks.fire.resolves([])
     })
 
     it('get users', async function (ctx) {
@@ -245,6 +248,7 @@ describe('UserMembershipController', function () {
     })
 
     it('render group managers view', async function (ctx) {
+      ctx.req.user = ctx.user
       ctx.req.entityConfig = EntityConfigs.groupManagers
       await ctx.UserMembershipController.manageGroupManagers(ctx.req, {
         render: (viewPath, viewParams) => {
@@ -255,6 +259,7 @@ describe('UserMembershipController', function () {
     })
 
     it('render institution view', async function (ctx) {
+      ctx.req.user = ctx.user
       ctx.req.entity = ctx.institution
       ctx.req.entityConfig = EntityConfigs.institution
       await ctx.UserMembershipController.manageInstitutionManagers(ctx.req, {

+ 1 - 0
services/web/types/admin-capabilities.ts

@@ -5,6 +5,7 @@ export type AdminCapability =
   | 'create-subscription'
   | 'modify-feature-override'
   | 'modify-group'
+  | 'modify-group-manager'
   | 'modify-group-member'
   | 'modify-group-setting'
   | 'modify-login-status'