diff --git a/backend/drive/test_item_sharing.py b/backend/drive/test_item_sharing.py index 0686d6f..d74ae24 100644 --- a/backend/drive/test_item_sharing.py +++ b/backend/drive/test_item_sharing.py @@ -128,3 +128,21 @@ class SharesTests(TestCase): users=self.client.get('/api/drive/admin/users/',{'q':'alice-admin-list'}) self.assertEqual(users.status_code,200,users.data) self.assertEqual(users.data['users'][0]['space_name'],self.root.display_name) + + def test_members_never_lists_viewer_as_direct_added_user(self): + """Hide redundant grants for the viewer or space owner, not real guests.""" + User = get_user_model() + delegate = User.objects.create_user('delegate', password='ComplexPassword123!') + guest = User.objects.create_user('real-guest', password='ComplexPassword123!') + SpaceGrant.objects.create(space=self.root, user=self.admin, can_invite=True) + SpaceGrant.objects.create(space=self.root, user=delegate, can_invite=True) + SpaceGrant.objects.create(space=self.root, user=guest) + + response = self.client.get('/api/drive/spaces/members/', {'parent_space': self.root.pk}) + self.assertEqual(response.status_code, 200, response.data) + self.assertEqual({g['user_id'] for g in response.data['grants']}, {delegate.pk, guest.pk}) + + self.client.force_authenticate(user=delegate) + response = self.client.get('/api/drive/spaces/members/', {'parent_space': self.root.pk}) + self.assertEqual(response.status_code, 200, response.data) + self.assertEqual([g['user_id'] for g in response.data['grants']], [guest.pk]) diff --git a/backend/drive/views_spaces.py b/backend/drive/views_spaces.py index c4301aa..cab99d3 100644 --- a/backend/drive/views_spaces.py +++ b/backend/drive/views_spaces.py @@ -93,9 +93,14 @@ def members(request): if not manager(request.user,parent):return error('Gestion des invitations non autorisée.',403) children=DriveSpace.objects.filter(parent=parent,private_owner__isnull=True).select_related('user','parent').order_by('id') invitations=SpaceInvitation.objects.filter(parent_space=parent).order_by('-created_at')[:100] + # Existing memberships of the viewer/space manager are not directly added users. + excluded_ids = {request.user.pk, parent.user_id, parent.managed_by_id} + grants = (SpaceGrant.objects.filter(space=parent) + .exclude(user_id__in=[pk for pk in excluded_ids if pk is not None]) + .select_related('user').order_by('user__username')) return Response({'parent':space_info(parent,request.user), 'children':[space_info(c,request.user) for c in children if not c.private_owner_id], - 'grants':[grant_info(g) for g in SpaceGrant.objects.filter(space=parent).select_related('user').order_by('user__username')], + 'grants':[grant_info(g) for g in grants], 'invitations':[invitation_info(i) for i in invitations]}) diff --git a/frontend/src/components/drive/DriveExplorer.jsx b/frontend/src/components/drive/DriveExplorer.jsx index b4a7de5..4310550 100644 --- a/frontend/src/components/drive/DriveExplorer.jsx +++ b/frontend/src/components/drive/DriveExplorer.jsx @@ -42,10 +42,8 @@ export default function DriveExplorer({ controller, user }) {
{loading ? 'Chargement…' : `${items.length} élément${items.length > 1 ? 's' : ''}`} diff --git a/frontend/src/pages/MembersPage.jsx b/frontend/src/pages/MembersPage.jsx index 1285b11..93ba1ba 100644 --- a/frontend/src/pages/MembersPage.jsx +++ b/frontend/src/pages/MembersPage.jsx @@ -43,6 +43,11 @@ export default function MembersPage({ controller, user, embedded = false }) { const selectedManagedSpace = managed.find(space => space.id === parentId) // Never render stale information from a previously selected parent space. const selectedRecord = record?.parent?.id === parentId ? record : null + // An account must never appear among its own directly added members. + const directGrants = (selectedRecord?.grants || []).filter(grant => + Number(grant.user_id) !== Number(user.id) && + Number(grant.user_id) !== Number(selectedRecord.parent.user_id) + ) const managedKey = managed.map(space => space.id).join(',') useEffect(() => { if (managedSpaceId) setParentId(managedSpaceId) }, [managedSpaceId]) useEffect(() => { @@ -178,8 +183,8 @@ export default function MembersPage({ controller, user, embedded = false }) {