[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
This commit is contained in:
Antoine Clausse
2025-08-20 10:57:57 +02:00
committed by Copybot
parent aab4b06f03
commit af44f478b9
13 changed files with 89 additions and 50 deletions
@@ -121,6 +121,7 @@ async function _renderManagersPage(req, res, next, template) {
name: entityName,
users,
groupId: entityPrimaryKey,
entityAccess: UserMembershipAuthorization.hasEntityAccess()(req),
})
}
@@ -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'),
@@ -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
)
@@ -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
@@ -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
@@ -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
@@ -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>
@@ -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">
@@ -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 />)
@@ -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 />)
@@ -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 />)
@@ -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
View File
@@ -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'