Skip to content

Commit

Permalink
Add guest permission in GET apis.
Browse files Browse the repository at this point in the history
  • Loading branch information
Rup-Narayan-Rajbanshi committed Jul 18, 2024
1 parent e04a84d commit 383e133
Show file tree
Hide file tree
Showing 11 changed files with 115 additions and 37 deletions.
3 changes: 2 additions & 1 deletion api/drf_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -889,6 +889,7 @@ def get_queryset(self):
detail=False,
url_path="me",
serializer_class=UserMeSerializer,
permission_classes = (IsAuthenticated, )
)
def get_authenticated_user_info(self, request, *args, **kwargs):
return Response(self.get_serializer_class()(request.user).data)
Expand Down Expand Up @@ -1309,7 +1310,7 @@ class UsersViewset(viewsets.ReadOnlyModelViewSet):
"""

serializer_class = UserSerializer
permission_classes = [IsAuthenticated]
permission_classes = [IsAuthenticated, DenyGuestUserMutationPermission]
filterset_class = UserFilterSet

def get_queryset(self):
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# Generated by Django 4.2.13 on 2024-07-12 06:47
# Generated by Django 4.2.13 on 2024-07-17 08:42

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("api", "0210_profile_accepted_montandon_license_terms"),
("api", "0211_alter_countrydirectory_unique_together_and_more"),
]

operations = [
Expand Down
93 changes: 86 additions & 7 deletions api/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,20 +35,21 @@ def test_guest_user_permission(self):
"/api/v2/add_subscription/",
"/api/v2/del_subscription/",
"/api/v2/external-token/",
"/api/v2/user/me/",
]

id = 1
go_apis = [
"/api/v2/dref/",
"/api/v2/dref-final-report/",
"/api/v2/dref-final-report/{id}/publish/",
f"/api/v2/dref-final-report/{id}/publish/",
"/api/v2/dref-op-update/",
"/api/v2/dref-op-update/{id}/publish/",
f"/api/v2/dref-op-update/{id}/publish/",
"/api/v2/dref-share/",
"/api/v2/dref/{id}/publish/",
f"/api/v2/dref/{id}/publish/",
"/api/v2/flash-update/",
"/api/v2/flash-update-file/multiple/",
"/api/v2/local-units/",
"/api/v2/local-units/{id}/validate/",
f"/api/v2/local-units/{id}/validate/",
"/api/v2/pdf-export/",
"/api/v2/per-assessment/",
"/api/v2/per-document-upload/",
Expand All @@ -67,30 +68,108 @@ def test_guest_user_permission(self):
"/api/v2/subscription/",
"/api/v2/user/",
]
country__iso__iexact = "NPL"

get_apis = [
"/api/v2/active-dref/",
f"/api/v2/active-dref/{id}/",
"/api/v2/aggregated-per-process-status/",
f"/api/v2/aggregated-per-process-status/{id}/",
"/api/v2/completed-dref/",
f"/api/v2/data-bank/country-overview/{country__iso__iexact}/",
"/api/v2/dref/",
"/api/v2/dref-files/",
"/api/v2/dref-final-report/",
f"/api/v2/dref-final-report/{id}/",
"/api/v2/dref-op-update/",
"/api/v2/dref-share-user/",
f"/api/v2/dref-share-user/{id}/",
f"/api/v2/dref/{id}/",
"/api/v2/field-report/",
f"/api/v2/field-report/{id}/",
"/api/v2/flash-update/",
"/api/v2/flash-update-file/",
f"/api/v2/flash-update/{id}/",
"/api/v2/language/",
f"/api/v2/language/{id}/",
"/api/v2/local-units/",
f"/api/v2/local-units/{id}/",
"/api/v2/ops-learning/",
f"/api/v2/ops-learning/{id}/",
f"/api/v2/pdf-export/{id}/",
"/api/v2/per-assessment/",
f"/api/v2/per-assessment/{id}/",
"/api/v2/per-document-upload/",
f"/api/v2/per-document-upload/{id}/",
"/api/v2/per-file/",
"/api/v2/per-options/",
"/api/v2/per-overview/",
f"/api/v2/per-overview/{id}/",
"/api/v2/per-prioritization/",
f"/api/v2/per-prioritization/{id}/",
"/api/v2/per-process-status/",
f"/api/v2/per-process-status/{id}/",
"/api/v2/per-stats/",
"/api/v2/per-work-plan/",
f"/api/v2/per-work-plan/{id}/",
"/api/v2/perdocs/",
f"/api/v2/perdocs/{id}/",
"/api/v2/personnel_deployment/",
f"/api/v2/personnel_deployment/{id}/",
"/api/v2/profile/",
f"/api/v2/profile/{id}/",
f"/api/v2/share-flash-update/{id}/",
"/api/v2/subscription/",
f"/api/v2/subscription/{id}/",
"/api/v2/user/",
f"/api/v2/user/{id}/",
"/api/v2/users/",
f"/api/v2/users/{id}/",
]
# TODO Add test case for export apis
# get_export_apis = [
# f"/api/v2/export-flash-update/{1}/",
# f"/api/v2/export-per/{1}/",
# ]

go_apis_req_additional_perm = [
"/api/v2/ops-learning/",
"/api/v2/per-overview/",
"/api/v2/user/{id}/accepted_license_terms/",
"/api/v2/language/{id}/bulk-action/",
f"/api/v2/user/{id}/accepted_license_terms/",
f"/api/v2/language/{id}/bulk-action/",
]

self.authenticate(user=self.guest_user)

# Guest user should not be able to access get apis that requires IsAuthenticated permission
for api_url in get_apis:
response = self.client.get(api_url).json()
error_code = response.get("error_code")
self.assertIn(error_code, [403, 401])

# Guest user should not be able to hit post apis.
for api_url in go_apis + go_apis_req_additional_perm:
response = self.client.post(api_url, json=body).json()
self.assertIn(response["error_code"], [401, 403])

# Guest user should be able to access guest apis
for api_url in guest_apis:
response = self.client.post(api_url, json=body).json()
error_code = response.get("error_code", None)
self.assertNotIn(error_code, [403, 401])

# Go user should be able to access go_apis
self.authenticate(user=self.go_user)
for api_url in go_apis:
response = self.client.post(api_url, json=body).json()
error_code = response.get("error_code", None)
self.assertNotIn(error_code, [403, 401])

for api_url in get_apis:
response = self.client.get(api_url).json()
error_code = response.get("error_code", None)
self.assertNotIn(error_code, [403, 401])


class AuthTokenTest(APITestCase):
def setUp(self):
Expand Down
4 changes: 3 additions & 1 deletion databank/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
from rest_framework.authentication import BasicAuthentication, TokenAuthentication
from rest_framework.permissions import IsAuthenticated

from main.permissions import DenyGuestUserMutationPermission

from .filter_set import FDRSIncomeFilter
from .models import CountryOverview, FDRSIncome
from .serializers import CountryOverviewSerializer, FDRSIncomeSerializer
Expand All @@ -11,7 +13,7 @@ class CountryOverviewViewSet(mixins.RetrieveModelMixin, viewsets.GenericViewSet)
queryset = CountryOverview.objects.all()
# TODO: Use global authentication class
authentication_classes = (BasicAuthentication, TokenAuthentication)
permission_classes = (IsAuthenticated,)
permission_classes = (IsAuthenticated, DenyGuestUserMutationPermission)
serializer_class = CountryOverviewSerializer
lookup_field = "country__iso__iexact"

Expand Down
2 changes: 1 addition & 1 deletion deployments/drf_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ class Meta:

class PersonnelDeploymentViewset(viewsets.ReadOnlyModelViewSet):
authentication_classes = (TokenAuthentication,)
permission_classes = (IsAuthenticated,)
permission_classes = (IsAuthenticated, DenyGuestUserMutationPermission)
queryset = PersonnelDeployment.objects.all()
serializer_class = PersonnelDeploymentSerializer
filterset_class = PersonnelDeploymentFilter
Expand Down
6 changes: 3 additions & 3 deletions dref/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ def multiple_file(self, request, pk=None, version=None):

class CompletedDrefOperationsViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = CompletedDrefOperationsSerializer
permission_classes = [permissions.IsAuthenticated]
permission_classes = [permissions.IsAuthenticated, DenyGuestUserMutationPermission]
filterset_class = CompletedDrefOperationsFilterSet
queryset = DrefFinalReport.objects.filter(is_published=True).order_by("-created_at").distinct()

Expand All @@ -211,7 +211,7 @@ def get_queryset(self):

class ActiveDrefOperationsViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = MiniDrefSerializer
permission_classes = [permissions.IsAuthenticated]
permission_classes = [permissions.IsAuthenticated, DenyGuestUserMutationPermission]
filterset_class = ActiveDrefFilterSet
queryset = (
Dref.objects.prefetch_related("planned_interventions", "needs_identified", "national_society_actions", "users")
Expand Down Expand Up @@ -239,7 +239,7 @@ def post(self, request):


class DrefShareUserViewSet(viewsets.ReadOnlyModelViewSet):
permissions_classes = [permissions.IsAuthenticated]
permission_classes = [permissions.IsAuthenticated, DenyGuestUserMutationPermission]
serializer_class = DrefShareUserSerializer
filterset_class = DrefShareUserFilterSet

Expand Down
11 changes: 5 additions & 6 deletions flash_update/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
from unittest import mock

from django.conf import settings
from django.contrib.auth.models import User

import api.models as models
from deployments.factories.user import UserFactory
Expand All @@ -22,7 +21,7 @@
class FlashUpdateTest(APITestCase):

def setUp(self):
self.user = User.objects.create(username="jo")
self.user = UserFactory.create(username="jo")
self.country1 = models.Country.objects.create(name="abc")
self.country2 = models.Country.objects.create(name="xyz")
self.district1 = models.District.objects.create(name="test district1", country=self.country1)
Expand Down Expand Up @@ -142,7 +141,7 @@ def test_patch(self):
self.assertEqual(flash_id.share_with, FlashUpdate.FlashShareWith.IFRC_SECRETARIAT)

def test_get_flash_update(self):
user1 = User.objects.create(username="abc")
user1 = UserFactory.create(username="abc")
flash_update1, flash_update2, flash_update3 = FlashUpdateFactory.create_batch(3, created_by=user1)
self.client.force_authenticate(user=user1)
response1 = self.client.get("/api/v2/flash-update/").json()
Expand All @@ -158,7 +157,7 @@ def test_get_flash_update(self):
self.assertEqual(response["id"], flash_update1.id)

# try with another user
user2 = User.objects.create(username="xyz")
user2 = UserFactory.create(username="xyz")
self.client.force_authenticate(user=user2)
flash_update4, flash_update5 = FlashUpdateFactory.create_batch(2, created_by=user2)
response2 = self.client.get("/api/v2/flash-update/").json()
Expand All @@ -168,13 +167,13 @@ def test_get_flash_update(self):
self.assertNotIn([data["id"] for data in response2["results"]], [data["id"] for data in response1["results"]])

# try with users who has no any flash update created
user3 = User.objects.create(username="ram")
user3 = UserFactory.create(username="ram")
self.client.force_authenticate(user=user3)
response3 = self.client.get("/api/v2/flash-update/").json()
self.assertEqual(response3["count"], 5)

def test_filter(self):
user = User.objects.create(username="xyz")
user = UserFactory.create(username="xyz")
self.client.force_authenticate(user=user)
hazard_type1 = models.DisasterType.objects.create(name="disaster_type1")
hazard_type2 = models.DisasterType.objects.create(name="disaster_type2")
Expand Down
4 changes: 3 additions & 1 deletion flash_update/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,9 @@ class ShareFlashUpdateViewSet(mixins.CreateModelMixin, mixins.RetrieveModelMixin


class ExportFlashUpdateView(views.APIView):
permission_classes = [permissions.IsAuthenticated]
permission_classes = [
permissions.IsAuthenticated,
]

@extend_schema(request=None, responses=ExportFlashUpdateViewSerializer)
def get(self, request, pk, format=None):
Expand Down
4 changes: 2 additions & 2 deletions lang/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from unittest import mock

from django.conf import settings
from django.contrib.auth.models import Permission, User
from django.contrib.auth.models import Permission
from django.core import management
from django.test import override_settings

Expand Down Expand Up @@ -131,7 +131,7 @@ def test_bulk_action(self):
self.assertEqual(first_string_key, string_1["key"])

def test_user_me(self):
user = User.objects.create_user(
user = UserFactory.create(
username="[email protected]",
first_name="User",
last_name="Toot",
Expand Down
9 changes: 2 additions & 7 deletions main/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,12 @@ def has_object_permission(self, request, view, obj):

class DenyGuestUserMutationPermission(permissions.BasePermission):
"""
Custom permission to deny mutation actions for logged-in guest users.
Custom permission to deny mutation and query actions for logged-in guest users.
This permission class allows all safe (read-only) operations but restricts
any mutation (write, update, delete) operations if the user is a guest.
This permission class restricts all (read, write, update, delete) operations if the user is a guest.
"""

def _has_permission(self, request, view):
# Allow all safe methods (GET, HEAD, OPTIONS) which are non-mutating.
if request.method in permissions.SAFE_METHODS:
return True

# For mutation methods (POST, PUT, DELETE, etc.):
# Check if the user is authenticated.
if not bool(request.user and request.user.is_authenticated):
Expand Down
12 changes: 6 additions & 6 deletions per/drf_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ class PERDocsViewset(viewsets.ReadOnlyModelViewSet):
# Duplicate of FormDataViewset
queryset = NiceDocument.objects.all()
authentication_classes = (TokenAuthentication,)
permission_classes = (IsAuthenticated,)
permission_classes = (IsAuthenticated, DenyGuestUserMutationPermission)
get_request_user_regions = RegionRestrictedAdmin.get_request_user_regions
get_filtered_queryset = RegionRestrictedAdmin.get_filtered_queryset
filterset_class = PERDocsFilter
Expand Down Expand Up @@ -227,7 +227,7 @@ def get_queryset(self):
class CountryPerStatsViewset(mixins.ListModelMixin, viewsets.GenericViewSet):
serializer_class = LatestCountryOverviewSerializer
filterset_class = PerOverviewFilter
permission_classes = [IsAuthenticated]
permission_classes = [IsAuthenticated, DenyGuestUserMutationPermission]

def get_queryset(self):
return Overview.objects.select_related("country", "type_of_assessment").order_by("-created_at")
Expand All @@ -247,7 +247,7 @@ def get_queryset(self):


class ExportPerView(views.APIView):
permission_classes = [permissions.IsAuthenticated]
permission_classes = [permissions.IsAuthenticated, DenyGuestUserMutationPermission]

content_negotiation_class = SpreadSheetContentNegotiation

Expand Down Expand Up @@ -535,7 +535,7 @@ class PublicFormPrioritizationViewSet(viewsets.ReadOnlyModelViewSet):


class PerOptionsView(views.APIView):
permission_classes = [permissions.IsAuthenticated]
permission_classes = [permissions.IsAuthenticated, DenyGuestUserMutationPermission]
ordering_fields = "__all__"

@extend_schema(request=None, responses=PerOptionsSerializer)
Expand All @@ -554,7 +554,7 @@ def get(self, request, version=None):
class PerProcessStatusViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = PerProcessSerializer
filterset_class = PerOverviewFilter
permission_classes = [permissions.IsAuthenticated]
permission_classes = [permissions.IsAuthenticated, DenyGuestUserMutationPermission]
ordering_fields = "__all__"
get_request_user_regions = RegionRestrictedAdmin.get_request_user_regions
get_filtered_queryset = RegionRestrictedAdmin.get_filtered_queryset
Expand Down Expand Up @@ -639,7 +639,7 @@ def get_queryset(self):
class PerAggregatedViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = PerProcessSerializer
filterset_class = PerOverviewFilter
permission_classes = [permissions.IsAuthenticated]
permission_classes = [permissions.IsAuthenticated, DenyGuestUserMutationPermission]
ordering_fields = ["assessment_number", "phase", "date_of_assessment"]
get_request_user_regions = RegionRestrictedAdmin.get_request_user_regions
get_filtered_queryset = RegionRestrictedAdmin.get_filtered_queryset
Expand Down

0 comments on commit 383e133

Please sign in to comment.