mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-05-23 17:19:37 +02:00
[web] Add admin permissions modify-group-member and modify-managed-group-member (#27665)
* Add capability `modify-managed-group-member` & `modify-group-member` * Check `modify-managed-group-member` & `modify-group-member` (backend) * Check `modify-managed-group-member` & `modify-group-member` (frontend) * Update tests * Update with `ol-hasWriteAccess` flag * Update tests * Move functions to AdminAuthorizationHelper.js * Update import to fix build error * Add `ol-hasWriteAccess` to types * Use `hasAdminAccess()` instead of `req?.user?.isAdmin` * Add tests on `/manage/groups/:id/invites` depending on admin roles * Reuse `UserMembershipAuthorization.hasAdminCapability` * Fix: Add entityAccess check * Update unit test * Rename `hasAdminGroupMemberCapability` to `hasModifyGroupMemberCapability` * Remove useless and redundant `hasWriteAccess` check * Restore stub in afterEach GitOrigin-RevId: 4b6d83751121b43d4c19d0dbd82a4833cf7a6f24
This commit is contained in:
@@ -16,6 +16,14 @@ const UserMembershipAuthorization = {
|
||||
|
||||
hasAdminCapability,
|
||||
|
||||
hasModifyGroupMemberCapability(req, res) {
|
||||
return hasAdminCapability(
|
||||
req.entity.managedUsersEnabled
|
||||
? 'modify-managed-group-member'
|
||||
: 'modify-group-member'
|
||||
)(req, res)
|
||||
},
|
||||
|
||||
hasEntityAccess() {
|
||||
return req => {
|
||||
if (!req.entity) {
|
||||
|
||||
@@ -14,6 +14,7 @@ import { expressify } from '@overleaf/promise-utils'
|
||||
import PlansLocator from '../Subscription/PlansLocator.js'
|
||||
import RecurlyClient from '../Subscription/RecurlyClient.js'
|
||||
import Modules from '../../infrastructure/Modules.js'
|
||||
import UserMembershipAuthorization from './UserMembershipAuthorization.js'
|
||||
|
||||
async function manageGroupMembers(req, res, next) {
|
||||
const { entity: subscription, entityConfig } = req
|
||||
@@ -59,6 +60,7 @@ async function manageGroupMembers(req, res, next) {
|
||||
groupSSOActive: ssoConfig?.enabled,
|
||||
canUseFlexibleLicensing: plan?.canUseFlexibleLicensing,
|
||||
canUseAddSeatsFeature,
|
||||
entityAccess: UserMembershipAuthorization.hasEntityAccess()(req),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -51,6 +51,19 @@ const UserMembershipMiddleware = {
|
||||
]),
|
||||
],
|
||||
|
||||
requireGroupMemberManagementAccess: [
|
||||
AuthenticationController.requireLogin(),
|
||||
fetchEntityConfig('group'),
|
||||
fetchEntity(),
|
||||
requireEntity(),
|
||||
useAdminCapabilities,
|
||||
allowAccessIfAny([
|
||||
UserMembershipAuthorization.hasEntityAccess(),
|
||||
UserMembershipAuthorization.hasStaffAccess('groupManagement'),
|
||||
UserMembershipAuthorization.hasModifyGroupMemberCapability,
|
||||
]),
|
||||
],
|
||||
|
||||
requireGroupMetricsAccess: [
|
||||
AuthenticationController.requireLogin(),
|
||||
fetchEntityConfig('group'),
|
||||
|
||||
@@ -26,13 +26,13 @@ export default {
|
||||
)
|
||||
webRouter.post(
|
||||
'/manage/groups/:id/invites',
|
||||
UserMembershipMiddleware.requireGroupManagementAccess,
|
||||
UserMembershipMiddleware.requireGroupMemberManagementAccess,
|
||||
RateLimiterMiddleware.rateLimit(rateLimiters.createTeamInvite),
|
||||
TeamInvitesController.createInvite
|
||||
)
|
||||
webRouter.post(
|
||||
'/manage/groups/:id/resendInvite',
|
||||
UserMembershipMiddleware.requireGroupManagementAccess,
|
||||
UserMembershipMiddleware.requireGroupMemberManagementAccess,
|
||||
RateLimiterMiddleware.rateLimit(rateLimiters.createTeamInvite),
|
||||
TeamInvitesController.resendInvite
|
||||
)
|
||||
@@ -43,7 +43,7 @@ export default {
|
||||
)
|
||||
webRouter.delete(
|
||||
'/manage/groups/:id/invites/:email',
|
||||
UserMembershipMiddleware.requireGroupManagementAccess,
|
||||
UserMembershipMiddleware.requireGroupMemberManagementAccess,
|
||||
TeamInvitesController.revokeInvite
|
||||
)
|
||||
webRouter.get(
|
||||
|
||||
@@ -4,11 +4,13 @@ block entrypointVar
|
||||
- entrypoint = 'pages/user/subscription/group-management/group-members'
|
||||
|
||||
block append meta
|
||||
- var hasWriteAccess = entityAccess || (hasAdminAccess() && hasAdminCapability(managedUsersActive ? 'modify-managed-group-member' : 'modify-group-member')) || (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-groupSize' data-type='number' content=groupSize)
|
||||
meta(name='ol-hasWriteAccess' data-type='boolean' content=hasWriteAccess)
|
||||
meta(
|
||||
name='ol-managedUsersActive'
|
||||
data-type='boolean'
|
||||
|
||||
@@ -37,6 +37,7 @@ export default function GroupMembers() {
|
||||
const groupSize = getMeta('ol-groupSize')
|
||||
const canUseFlexibleLicensing = getMeta('ol-canUseFlexibleLicensing')
|
||||
const canUseAddSeatsFeature = getMeta('ol-canUseAddSeatsFeature')
|
||||
const hasWriteAccess = getMeta('ol-hasWriteAccess')
|
||||
|
||||
const handleEmailsChange = useCallback(
|
||||
(e: ChangeEvent<HTMLInputElement>) => {
|
||||
@@ -133,10 +134,10 @@ export default function GroupMembers() {
|
||||
</div>
|
||||
<div className="row-spaced-small">
|
||||
<ErrorAlert error={removeMemberError} />
|
||||
<MembersList groupId={groupId} />
|
||||
<MembersList groupId={groupId} hasWriteAccess={hasWriteAccess} />
|
||||
</div>
|
||||
<hr />
|
||||
{users.length < groupSize && (
|
||||
{hasWriteAccess && users.length < groupSize && (
|
||||
<div
|
||||
className="add-more-members-form"
|
||||
data-testid="add-more-members-form"
|
||||
@@ -186,7 +187,8 @@ export default function GroupMembers() {
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
{users.length >= groupSize && users.length > 0 && (
|
||||
{(!hasWriteAccess ||
|
||||
(users.length >= groupSize && users.length > 0)) && (
|
||||
<>
|
||||
<ErrorAlert error={inviteError} />
|
||||
<OLRow>
|
||||
|
||||
@@ -20,6 +20,7 @@ type ManagedUserRowProps = {
|
||||
openUnlinkUserModal: (user: User) => void
|
||||
groupId: string
|
||||
setGroupUserAlert: Dispatch<SetStateAction<GroupUserAlert>>
|
||||
hasWriteAccess: boolean
|
||||
}
|
||||
|
||||
export default function MemberRow({
|
||||
@@ -29,6 +30,7 @@ export default function MemberRow({
|
||||
openUnlinkUserModal,
|
||||
setGroupUserAlert,
|
||||
groupId,
|
||||
hasWriteAccess,
|
||||
}: ManagedUserRowProps) {
|
||||
const { t } = useTranslation()
|
||||
const managedUsersActive = getMeta('ol-managedUsersActive')
|
||||
@@ -36,7 +38,7 @@ export default function MemberRow({
|
||||
|
||||
return (
|
||||
<tr className="managed-entity-row">
|
||||
<SelectUserCheckbox user={user} />
|
||||
{hasWriteAccess && <SelectUserCheckbox user={user} />}
|
||||
<td
|
||||
className={classnames('cell-email', {
|
||||
'text-muted': user.invite,
|
||||
@@ -110,16 +112,18 @@ export default function MemberRow({
|
||||
</div>
|
||||
</td>
|
||||
)}
|
||||
<td className="cell-dropdown">
|
||||
<DropdownButton
|
||||
user={user}
|
||||
openOffboardingModalForUser={openOffboardingModalForUser}
|
||||
openRemoveModalForUser={openRemoveModalForUser}
|
||||
openUnlinkUserModal={openUnlinkUserModal}
|
||||
setGroupUserAlert={setGroupUserAlert}
|
||||
groupId={groupId}
|
||||
/>
|
||||
</td>
|
||||
{hasWriteAccess && (
|
||||
<td className="cell-dropdown">
|
||||
<DropdownButton
|
||||
user={user}
|
||||
openOffboardingModalForUser={openOffboardingModalForUser}
|
||||
openRemoveModalForUser={openRemoveModalForUser}
|
||||
openUnlinkUserModal={openUnlinkUserModal}
|
||||
setGroupUserAlert={setGroupUserAlert}
|
||||
groupId={groupId}
|
||||
/>
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ const USERS_DISPLAY_LIMIT = 50
|
||||
|
||||
type ManagedUsersListProps = {
|
||||
groupId: string
|
||||
hasWriteAccess: boolean
|
||||
}
|
||||
|
||||
function isUserSearchMatch(user: User, search: NonEmptyString): boolean {
|
||||
@@ -40,7 +41,10 @@ function isUserSearchMatch(user: User, search: NonEmptyString): boolean {
|
||||
)
|
||||
}
|
||||
|
||||
export default function MembersList({ groupId }: ManagedUsersListProps) {
|
||||
export default function MembersList({
|
||||
groupId,
|
||||
hasWriteAccess,
|
||||
}: ManagedUsersListProps) {
|
||||
const { t } = useTranslation()
|
||||
const [userToOffboard, setUserToOffboard] = useState<User | undefined>(
|
||||
undefined
|
||||
@@ -155,7 +159,7 @@ export default function MembersList({ groupId }: ManagedUsersListProps) {
|
||||
>
|
||||
<thead>
|
||||
<tr ref={tHeadRowRef}>
|
||||
<SelectAllCheckbox />
|
||||
{hasWriteAccess && <SelectAllCheckbox />}
|
||||
<th className="cell-email">{t('email')}</th>
|
||||
<th className="cell-name">{t('name')}</th>
|
||||
<th className="cell-last-active">
|
||||
@@ -178,7 +182,7 @@ export default function MembersList({ groupId }: ManagedUsersListProps) {
|
||||
{managedUsersActive && (
|
||||
<th className="cell-managed">{t('managed')}</th>
|
||||
)}
|
||||
<th />
|
||||
{hasWriteAccess && <th />}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -203,6 +207,7 @@ export default function MembersList({ groupId }: ManagedUsersListProps) {
|
||||
openUnlinkUserModal={setUserToUnlink}
|
||||
setGroupUserAlert={setGroupUserAlert}
|
||||
groupId={groupId}
|
||||
hasWriteAccess={hasWriteAccess}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
|
||||
@@ -43,6 +43,7 @@ describe('GroupMembers', function () {
|
||||
win.metaAttributesCache.set('ol-groupName', 'My Awesome Team')
|
||||
win.metaAttributesCache.set('ol-groupSize', 10)
|
||||
win.metaAttributesCache.set('ol-users', [JOHN_DOE, BOBBY_LAPOINTE])
|
||||
win.metaAttributesCache.set('ol-hasWriteAccess', true)
|
||||
})
|
||||
|
||||
cy.mount(
|
||||
@@ -243,6 +244,7 @@ describe('GroupMembers', function () {
|
||||
win.metaAttributesCache.set('ol-groupName', 'My Awesome Team')
|
||||
win.metaAttributesCache.set('ol-groupSize', 10)
|
||||
win.metaAttributesCache.set('ol-managedUsersActive', true)
|
||||
win.metaAttributesCache.set('ol-hasWriteAccess', true)
|
||||
})
|
||||
mountGroupMembersProvider()
|
||||
})
|
||||
@@ -514,6 +516,7 @@ describe('GroupMembers', function () {
|
||||
win.metaAttributesCache.set('ol-groupSize', 10)
|
||||
win.metaAttributesCache.set('ol-canUseFlexibleLicensing', true)
|
||||
win.metaAttributesCache.set('ol-canUseAddSeatsFeature', true)
|
||||
win.metaAttributesCache.set('ol-hasWriteAccess', true)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -77,6 +77,7 @@ describe('group members, with managed users', function () {
|
||||
win.metaAttributesCache.set('ol-groupName', 'My Awesome Team')
|
||||
win.metaAttributesCache.set('ol-groupSize', 10)
|
||||
win.metaAttributesCache.set('ol-managedUsersActive', true)
|
||||
win.metaAttributesCache.set('ol-hasWriteAccess', true)
|
||||
})
|
||||
mountGroupMembersProvider()
|
||||
})
|
||||
|
||||
@@ -34,6 +34,7 @@ describe('MemberRow', function () {
|
||||
openUnlinkUserModal={sinon.stub()}
|
||||
groupId={subscriptionId}
|
||||
setGroupUserAlert={sinon.stub()}
|
||||
hasWriteAccess
|
||||
/>
|
||||
</GroupMembersProvider>
|
||||
)
|
||||
@@ -87,6 +88,7 @@ describe('MemberRow', function () {
|
||||
openUnlinkUserModal={sinon.stub()}
|
||||
groupId={subscriptionId}
|
||||
setGroupUserAlert={sinon.stub()}
|
||||
hasWriteAccess
|
||||
/>
|
||||
</GroupMembersProvider>
|
||||
)
|
||||
@@ -127,6 +129,7 @@ describe('MemberRow', function () {
|
||||
openUnlinkUserModal={sinon.stub()}
|
||||
groupId={subscriptionId}
|
||||
setGroupUserAlert={sinon.stub()}
|
||||
hasWriteAccess
|
||||
/>
|
||||
</GroupMembersProvider>
|
||||
)
|
||||
@@ -166,6 +169,7 @@ describe('MemberRow', function () {
|
||||
openUnlinkUserModal={sinon.stub()}
|
||||
groupId={subscriptionId}
|
||||
setGroupUserAlert={sinon.stub()}
|
||||
hasWriteAccess
|
||||
/>
|
||||
</GroupMembersProvider>
|
||||
)
|
||||
@@ -215,6 +219,7 @@ describe('MemberRow', function () {
|
||||
openUnlinkUserModal={sinon.stub()}
|
||||
groupId={subscriptionId}
|
||||
setGroupUserAlert={sinon.stub()}
|
||||
hasWriteAccess
|
||||
/>
|
||||
</GroupMembersProvider>
|
||||
)
|
||||
@@ -267,6 +272,7 @@ describe('MemberRow', function () {
|
||||
openUnlinkUserModal={sinon.stub()}
|
||||
groupId={subscriptionId}
|
||||
setGroupUserAlert={sinon.stub()}
|
||||
hasWriteAccess
|
||||
/>
|
||||
</GroupMembersProvider>
|
||||
)
|
||||
@@ -307,6 +313,7 @@ describe('MemberRow', function () {
|
||||
openUnlinkUserModal={sinon.stub()}
|
||||
groupId={subscriptionId}
|
||||
setGroupUserAlert={sinon.stub()}
|
||||
hasWriteAccess
|
||||
/>
|
||||
</GroupMembersProvider>
|
||||
)
|
||||
@@ -346,6 +353,7 @@ describe('MemberRow', function () {
|
||||
openUnlinkUserModal={sinon.stub()}
|
||||
groupId={subscriptionId}
|
||||
setGroupUserAlert={sinon.stub()}
|
||||
hasWriteAccess
|
||||
/>
|
||||
</GroupMembersProvider>
|
||||
)
|
||||
@@ -395,6 +403,7 @@ describe('MemberRow', function () {
|
||||
openUnlinkUserModal={sinon.stub()}
|
||||
groupId={subscriptionId}
|
||||
setGroupUserAlert={sinon.stub()}
|
||||
hasWriteAccess
|
||||
/>
|
||||
</GroupMembersProvider>
|
||||
)
|
||||
@@ -448,6 +457,7 @@ describe('MemberRow', function () {
|
||||
openUnlinkUserModal={sinon.stub()}
|
||||
groupId={subscriptionId}
|
||||
setGroupUserAlert={sinon.stub()}
|
||||
hasWriteAccess
|
||||
/>
|
||||
</GroupMembersProvider>
|
||||
)
|
||||
@@ -488,6 +498,7 @@ describe('MemberRow', function () {
|
||||
openUnlinkUserModal={sinon.stub()}
|
||||
groupId={subscriptionId}
|
||||
setGroupUserAlert={sinon.stub()}
|
||||
hasWriteAccess
|
||||
/>
|
||||
</GroupMembersProvider>
|
||||
)
|
||||
@@ -527,6 +538,7 @@ describe('MemberRow', function () {
|
||||
openUnlinkUserModal={sinon.stub()}
|
||||
groupId={subscriptionId}
|
||||
setGroupUserAlert={sinon.stub()}
|
||||
hasWriteAccess
|
||||
/>
|
||||
</GroupMembersProvider>
|
||||
)
|
||||
@@ -577,6 +589,7 @@ describe('MemberRow', function () {
|
||||
openUnlinkUserModal={sinon.stub()}
|
||||
groupId={subscriptionId}
|
||||
setGroupUserAlert={sinon.stub()}
|
||||
hasWriteAccess
|
||||
/>
|
||||
</GroupMembersProvider>
|
||||
)
|
||||
@@ -630,6 +643,7 @@ describe('MemberRow', function () {
|
||||
openUnlinkUserModal={sinon.stub()}
|
||||
groupId={subscriptionId}
|
||||
setGroupUserAlert={sinon.stub()}
|
||||
hasWriteAccess
|
||||
/>
|
||||
</GroupMembersProvider>
|
||||
)
|
||||
@@ -670,6 +684,7 @@ describe('MemberRow', function () {
|
||||
openUnlinkUserModal={sinon.stub()}
|
||||
groupId={subscriptionId}
|
||||
setGroupUserAlert={sinon.stub()}
|
||||
hasWriteAccess
|
||||
/>
|
||||
</GroupMembersProvider>
|
||||
)
|
||||
@@ -709,6 +724,7 @@ describe('MemberRow', function () {
|
||||
openUnlinkUserModal={sinon.stub()}
|
||||
groupId={subscriptionId}
|
||||
setGroupUserAlert={sinon.stub()}
|
||||
hasWriteAccess
|
||||
/>
|
||||
</GroupMembersProvider>
|
||||
)
|
||||
|
||||
@@ -7,7 +7,7 @@ const groupId = 'somegroup'
|
||||
function mountManagedUsersList() {
|
||||
cy.mount(
|
||||
<GroupMembersProvider>
|
||||
<MembersList groupId={groupId} />
|
||||
<MembersList groupId={groupId} hasWriteAccess />
|
||||
</GroupMembersProvider>
|
||||
)
|
||||
}
|
||||
@@ -166,7 +166,7 @@ describe('MembersList', function () {
|
||||
})
|
||||
cy.mount(
|
||||
<GroupMembersProvider>
|
||||
<MembersList groupId={groupId} />
|
||||
<MembersList groupId={groupId} hasWriteAccess />
|
||||
</GroupMembersProvider>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -35,6 +35,7 @@ describe('UserMembershipController', function () {
|
||||
ctx.subscription = {
|
||||
_id: 'mock-subscription-id',
|
||||
admin_id: 'mock-admin-id',
|
||||
manager_ids: ['mock-admin-id'],
|
||||
fetchV1Data: callback => callback(null, ctx.subscription),
|
||||
}
|
||||
ctx.institution = {
|
||||
@@ -201,6 +202,7 @@ describe('UserMembershipController', function () {
|
||||
|
||||
describe('index', function () {
|
||||
beforeEach(function (ctx) {
|
||||
ctx.req.user = ctx.user
|
||||
ctx.req.entity = ctx.subscription
|
||||
ctx.req.entityConfig = EntityConfigs.group
|
||||
})
|
||||
|
||||
@@ -5,9 +5,11 @@ export type AdminCapability =
|
||||
| 'create-subscription'
|
||||
| 'modify-feature-override'
|
||||
| 'modify-group'
|
||||
| 'modify-group-member'
|
||||
| 'modify-group-setting'
|
||||
| 'modify-login-status'
|
||||
| 'modify-managed-group'
|
||||
| 'modify-managed-group-member'
|
||||
| 'modify-project'
|
||||
| 'manage-survey'
|
||||
| 'modify-split-test'
|
||||
|
||||
Reference in New Issue
Block a user