Skip to content

Commit 4682933

Browse files
mihowclaude
andcommitted
fix: use IsProjectMemberOrReadOnly for TaxaListViewSet permissions
ObjectPermission doesn't work for M2M-to-project models because BaseModel.get_project() returns None when get_project_accessor() returns "projects". This caused all write operations (create, update, delete) on taxa lists to be denied for every user. Switch to IsProjectMemberOrReadOnly which resolves the project via ProjectMixin.get_active_project() (from query param) instead of through the model instance. Add 10 permission tests covering member CRUD, anonymous read-only, non-member rejection, and owner access. Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 8f5144e commit 4682933

2 files changed

Lines changed: 52 additions & 3 deletions

File tree

ami/main/api/views.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1634,7 +1634,7 @@ class TaxaListViewSet(DefaultViewSet, ProjectMixin):
16341634
"created_at",
16351635
"updated_at",
16361636
]
1637-
permission_classes = [ObjectPermission]
1637+
permission_classes = [IsProjectMemberOrReadOnly]
16381638
require_project = True
16391639

16401640
def get_queryset(self):
@@ -1652,8 +1652,6 @@ def perform_create(self, serializer):
16521652
16531653
Users cannot manually assign taxa lists to projects for security reasons.
16541654
A taxa list is always created in the context of the active project.
1655-
1656-
@TODO Do we need to check permissions here? Is this user allowed to add taxa lists to this project?
16571655
"""
16581656
instance = serializer.save()
16591657
project = self.get_active_project()

ami/main/tests.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3445,6 +3445,57 @@ def test_taxon_detail_visible_when_excluded_from_list(self):
34453445
self.assertEqual(res.status_code, status.HTTP_200_OK)
34463446

34473447

3448+
class TaxaListViewSetPermissionTestCase(TestCase):
3449+
"""Test TaxaListViewSet write permissions for project members vs non-members."""
3450+
3451+
def setUp(self):
3452+
self.owner = User.objects.create_user(email="owner@example.com", password="testpass")
3453+
self.member = User.objects.create_user(email="member@example.com", password="testpass")
3454+
self.non_member = User.objects.create_user(email="nonmember@example.com", password="testpass")
3455+
self.project = Project.objects.create(name="Test Project", owner=self.owner)
3456+
self.project.members.add(self.member)
3457+
self.taxa_list = TaxaList.objects.create(name="Existing List", description="A list")
3458+
self.taxa_list.projects.add(self.project)
3459+
self.client = APIClient()
3460+
self.list_url = f"/api/v2/taxa/lists/?project_id={self.project.pk}"
3461+
self.detail_url = f"/api/v2/taxa/lists/{self.taxa_list.pk}/?project_id={self.project.pk}"
3462+
3463+
def test_member_can_create_taxa_list(self):
3464+
self.client.force_authenticate(self.member)
3465+
response = self.client.post(self.list_url, {"name": "New List"})
3466+
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
3467+
3468+
def test_member_can_update_taxa_list(self):
3469+
self.client.force_authenticate(self.member)
3470+
response = self.client.patch(self.detail_url, {"name": "Renamed"})
3471+
self.assertEqual(response.status_code, status.HTTP_200_OK)
3472+
self.taxa_list.refresh_from_db()
3473+
self.assertEqual(self.taxa_list.name, "Renamed")
3474+
3475+
def test_member_can_delete_taxa_list(self):
3476+
self.client.force_authenticate(self.member)
3477+
response = self.client.delete(self.detail_url)
3478+
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
3479+
3480+
def test_anonymous_cannot_create_taxa_list(self):
3481+
response = self.client.post(self.list_url, {"name": "Anon List"})
3482+
self.assertIn(response.status_code, [status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN])
3483+
3484+
def test_anonymous_cannot_update_taxa_list(self):
3485+
response = self.client.patch(self.detail_url, {"name": "Hacked"})
3486+
self.assertIn(response.status_code, [status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN])
3487+
3488+
def test_non_member_cannot_create_taxa_list(self):
3489+
self.client.force_authenticate(self.non_member)
3490+
response = self.client.post(self.list_url, {"name": "Intruder List"})
3491+
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
3492+
3493+
def test_non_member_cannot_update_taxa_list(self):
3494+
self.client.force_authenticate(self.non_member)
3495+
response = self.client.patch(self.detail_url, {"name": "Hacked"})
3496+
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
3497+
3498+
34483499
class TaxaListTaxonAPITestCase(TestCase):
34493500
"""Test TaxaList taxa management operations via API."""
34503501

0 commit comments

Comments
 (0)