-
-
Notifications
You must be signed in to change notification settings - Fork 301
fix: restrict invitation accept/decline actions to invited user only #5893
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: unstable
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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): | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. praise: Adding |
||
| 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) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion: The
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added the required test,also added test_decline_invitation_by_admin_succeeds for symmetry, since _ensure_invitee is called by both actions. |
||
| 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( | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
praise: Using
refresh_from_db()after the request to assert the DB state wasn't mutated is exactly the right way to write these tests — it catches cases where the view returns the right status code but fails to persist (or fails to NOT persist) the change. All four new tests follow this pattern.