Skip to content

Commit 0737355

Browse files
fix: restrict invitation accept/decline actions to invited user only (learningequality#5893)
* Resolved the issue * FIX issue * added tests * updated accordign to the review
1 parent f77f675 commit 0737355

2 files changed

Lines changed: 107 additions & 2 deletions

File tree

contentcuration/contentcuration/tests/viewsets/test_invitation.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -446,3 +446,93 @@ def test_update_invitation_decline(self):
446446
).exists()
447447
)
448448
self.assertTrue(models.Change.objects.filter(channel=self.channel).exists())
449+
450+
def test_accept_invitation_by_channel_editor_is_forbidden(self):
451+
invitation = models.Invitation.objects.create(**self.invitation_db_metadata)
452+
453+
self.client.force_authenticate(user=self.user)
454+
response = self.client.post(
455+
reverse("invitation-accept", kwargs={"pk": invitation.id})
456+
)
457+
self.assertEqual(response.status_code, 403, response.content)
458+
invitation.refresh_from_db()
459+
self.assertFalse(invitation.accepted)
460+
461+
def test_decline_invitation_by_channel_editor_is_forbidden(self):
462+
invitation = models.Invitation.objects.create(**self.invitation_db_metadata)
463+
464+
self.client.force_authenticate(user=self.user)
465+
response = self.client.post(
466+
reverse("invitation-decline", kwargs={"pk": invitation.id})
467+
)
468+
self.assertEqual(response.status_code, 403, response.content)
469+
invitation.refresh_from_db()
470+
self.assertFalse(invitation.declined)
471+
472+
def test_accept_invitation_by_unrelated_user_is_not_found(self):
473+
invitation = models.Invitation.objects.create(**self.invitation_db_metadata)
474+
unrelated_user = testdata.user("unrelated@example.com")
475+
476+
self.client.force_authenticate(user=unrelated_user)
477+
response = self.client.post(
478+
reverse("invitation-accept", kwargs={"pk": invitation.id})
479+
)
480+
self.assertEqual(response.status_code, 404, response.content)
481+
invitation.refresh_from_db()
482+
self.assertFalse(invitation.accepted)
483+
484+
def test_decline_invitation_by_unrelated_user_is_not_found(self):
485+
invitation = models.Invitation.objects.create(**self.invitation_db_metadata)
486+
unrelated_user = testdata.user("unrelated@example.com")
487+
488+
self.client.force_authenticate(user=unrelated_user)
489+
response = self.client.post(
490+
reverse("invitation-decline", kwargs={"pk": invitation.id})
491+
)
492+
self.assertEqual(response.status_code, 404, response.content)
493+
invitation.refresh_from_db()
494+
self.assertFalse(invitation.declined)
495+
496+
def _make_admin(self, email="admin@example.com"):
497+
user = testdata.user(email)
498+
user.is_admin = True
499+
user.save()
500+
return user
501+
502+
def test_accept_invitation_by_admin_succeeds(self):
503+
invitation = models.Invitation.objects.create(**self.invitation_db_metadata)
504+
admin_user = self._make_admin()
505+
506+
self.client.force_authenticate(user=admin_user)
507+
response = self.client.post(
508+
reverse("invitation-accept", kwargs={"pk": invitation.id})
509+
)
510+
self.assertEqual(response.status_code, 200, response.content)
511+
invitation.refresh_from_db()
512+
self.assertTrue(invitation.accepted)
513+
514+
def test_decline_invitation_by_admin_succeeds(self):
515+
invitation = models.Invitation.objects.create(**self.invitation_db_metadata)
516+
admin_user = self._make_admin()
517+
518+
self.client.force_authenticate(user=admin_user)
519+
response = self.client.post(
520+
reverse("invitation-decline", kwargs={"pk": invitation.id})
521+
)
522+
self.assertEqual(response.status_code, 200, response.content)
523+
invitation.refresh_from_db()
524+
self.assertTrue(invitation.declined)
525+
526+
def test_accept_revoked_invitation_returns_400(self):
527+
invitation = models.Invitation.objects.create(**self.invitation_db_metadata)
528+
invitation.revoked = True
529+
invitation.save()
530+
531+
self.client.force_authenticate(user=self.invited_user)
532+
response = self.client.post(
533+
reverse("invitation-accept", kwargs={"pk": invitation.id})
534+
)
535+
self.assertEqual(response.status_code, 400, response.content)
536+
invitation.refresh_from_db()
537+
self.assertFalse(invitation.accepted)
538+
self.assertFalse(self.channel.editors.filter(pk=self.invited_user.id).exists())

contentcuration/contentcuration/viewsets/invitation.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
from django_filters.rest_framework import CharFilter
22
from django_filters.rest_framework import FilterSet
33
from rest_framework import serializers
4+
from rest_framework import status
45
from rest_framework.decorators import action
6+
from rest_framework.exceptions import PermissionDenied
57
from rest_framework.permissions import IsAuthenticated
68
from rest_framework.response import Response
79

@@ -137,9 +139,21 @@ def perform_update(self, serializer):
137139
instance = serializer.save()
138140
instance.save()
139141

142+
def _ensure_invitee(self, request, invitation):
143+
if request.user.is_admin:
144+
return
145+
if (request.user.email or "").lower() != (invitation.email or "").lower():
146+
raise PermissionDenied("Only the invited user may perform this action.")
147+
140148
@action(detail=True, methods=["post"])
141149
def accept(self, request, pk=None):
142-
invitation = self.get_object()
150+
invitation = self.get_edit_object()
151+
self._ensure_invitee(request, invitation)
152+
if invitation.revoked:
153+
return Response(
154+
"Invitation has been revoked",
155+
status=status.HTTP_400_BAD_REQUEST,
156+
)
143157
invitation.accept()
144158
invitation.accepted = True
145159
invitation.save()
@@ -157,7 +171,8 @@ def accept(self, request, pk=None):
157171

158172
@action(detail=True, methods=["post"])
159173
def decline(self, request, pk=None):
160-
invitation = self.get_object()
174+
invitation = self.get_edit_object()
175+
self._ensure_invitee(request, invitation)
161176
invitation.declined = True
162177
invitation.save()
163178
Change.create_change(

0 commit comments

Comments
 (0)