diff --git a/contentcuration/contentcuration/tests/viewsets/test_invitation.py b/contentcuration/contentcuration/tests/viewsets/test_invitation.py index f044f50a99..e5eed46387 100644 --- a/contentcuration/contentcuration/tests/viewsets/test_invitation.py +++ b/contentcuration/contentcuration/tests/viewsets/test_invitation.py @@ -446,3 +446,77 @@ def test_update_invitation_decline(self): ).exists() ) self.assertTrue(models.Change.objects.filter(channel=self.channel).exists()) + + def test_accept_invitation_by_channel_editor_is_forbidden(self): + invitation = models.Invitation.objects.create(**self.invitation_db_metadata) + + self.client.force_authenticate(user=self.user) + response = self.client.post( + reverse("invitation-accept", kwargs={"pk": invitation.id}) + ) + self.assertEqual(response.status_code, 403, response.content) + invitation.refresh_from_db() + self.assertFalse(invitation.accepted) + + def test_decline_invitation_by_channel_editor_is_forbidden(self): + invitation = models.Invitation.objects.create(**self.invitation_db_metadata) + + self.client.force_authenticate(user=self.user) + response = self.client.post( + reverse("invitation-decline", kwargs={"pk": invitation.id}) + ) + self.assertEqual(response.status_code, 403, response.content) + invitation.refresh_from_db() + self.assertFalse(invitation.declined) + + def test_accept_invitation_by_unrelated_user_is_not_found(self): + invitation = models.Invitation.objects.create(**self.invitation_db_metadata) + unrelated_user = testdata.user("unrelated@example.com") + + self.client.force_authenticate(user=unrelated_user) + response = self.client.post( + reverse("invitation-accept", kwargs={"pk": invitation.id}) + ) + self.assertEqual(response.status_code, 404, response.content) + invitation.refresh_from_db() + self.assertFalse(invitation.accepted) + + def test_decline_invitation_by_unrelated_user_is_not_found(self): + invitation = models.Invitation.objects.create(**self.invitation_db_metadata) + unrelated_user = testdata.user("unrelated@example.com") + + self.client.force_authenticate(user=unrelated_user) + response = self.client.post( + reverse("invitation-decline", kwargs={"pk": invitation.id}) + ) + self.assertEqual(response.status_code, 404, response.content) + invitation.refresh_from_db() + self.assertFalse(invitation.declined) + + def test_accept_invitation_by_admin_succeeds(self): + invitation = models.Invitation.objects.create(**self.invitation_db_metadata) + admin_user = testdata.user("admin@example.com") + admin_user.is_admin = True + admin_user.save() + + self.client.force_authenticate(user=admin_user) + response = self.client.post( + reverse("invitation-accept", kwargs={"pk": invitation.id}) + ) + self.assertEqual(response.status_code, 200, response.content) + invitation.refresh_from_db() + self.assertTrue(invitation.accepted) + + def test_decline_invitation_by_admin_succeeds(self): + invitation = models.Invitation.objects.create(**self.invitation_db_metadata) + admin_user = testdata.user("admin@example.com") + admin_user.is_admin = True + admin_user.save() + + self.client.force_authenticate(user=admin_user) + response = self.client.post( + reverse("invitation-decline", kwargs={"pk": invitation.id}) + ) + self.assertEqual(response.status_code, 200, response.content) + invitation.refresh_from_db() + self.assertTrue(invitation.declined) diff --git a/contentcuration/contentcuration/viewsets/invitation.py b/contentcuration/contentcuration/viewsets/invitation.py index 7d8ff577f6..cd500cd74c 100644 --- a/contentcuration/contentcuration/viewsets/invitation.py +++ b/contentcuration/contentcuration/viewsets/invitation.py @@ -2,6 +2,7 @@ from django_filters.rest_framework import FilterSet from rest_framework import serializers from rest_framework.decorators import action +from rest_framework.exceptions import PermissionDenied from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response @@ -137,9 +138,16 @@ def perform_update(self, serializer): instance = serializer.save() instance.save() + def _ensure_invitee(self, request, invitation): + if request.user.is_admin: + return + if (request.user.email or "").lower() != (invitation.email or "").lower(): + raise PermissionDenied("Only the invited user may perform this action.") + @action(detail=True, methods=["post"]) def accept(self, request, pk=None): - invitation = self.get_object() + invitation = self.get_edit_object() + self._ensure_invitee(request, invitation) invitation.accept() invitation.accepted = True invitation.save() @@ -157,7 +165,8 @@ def accept(self, request, pk=None): @action(detail=True, methods=["post"]) def decline(self, request, pk=None): - invitation = self.get_object() + invitation = self.get_edit_object() + self._ensure_invitee(request, invitation) invitation.declined = True invitation.save() Change.create_change(