Просмотр исходного кода

[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
Antoine Clausse 1 год назад
Родитель
Сommit
40772ef819

+ 8 - 0
services/web/app/src/Features/UserMembership/UserMembershipAuthorization.js

@@ -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) {

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

@@ -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),
   })
 }
 

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

@@ -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'),

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

@@ -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(

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

@@ -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'

+ 5 - 3
services/web/frontend/js/features/group-management/components/group-members.tsx

@@ -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>

+ 15 - 11
services/web/frontend/js/features/group-management/components/members-table/member-row.tsx

@@ -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>
   )
 }

+ 8 - 3
services/web/frontend/js/features/group-management/components/members-table/members-list.tsx

@@ -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>

+ 3 - 0
services/web/test/frontend/features/group-management/components/group-members.spec.tsx

@@ -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)
       })
     })
 

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

@@ -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()
   })

+ 16 - 0
services/web/test/frontend/features/group-management/components/members-table/member-row.spec.tsx

@@ -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>
         )

+ 2 - 2
services/web/test/frontend/features/group-management/components/members-table/members-list.spec.tsx

@@ -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>
       )
     })

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

@@ -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
     })

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

@@ -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'