Skip to content

Commit

Permalink
Merge pull request #2183 from IFRCGo/feature/user-guest-permission
Browse files Browse the repository at this point in the history
Create Guest User Permission with Public Content Access
  • Loading branch information
samshara authored Aug 7, 2024
2 parents f3937bd + ec0e3b1 commit ff82180
Show file tree
Hide file tree
Showing 23 changed files with 314 additions and 73 deletions.
1 change: 0 additions & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ exclude: |
(?x)^(
\.git|
__pycache__|
.*snap_test_.*\.py|
.+\/.+\/migrations\/.*|
legacy|
\.venv
Expand Down
17 changes: 7 additions & 10 deletions api/drf_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
from deployments.models import Personnel
from main.enums import GlobalEnumSerializer, get_enum_values
from main.filters import NullsLastOrderingFilter
from main.permissions import DenyGuestUserMutationPermission
from main.utils import is_tableau
from per.models import Overview
from per.serializers import CountryLatestOverviewSerializer
Expand Down Expand Up @@ -870,7 +871,7 @@ def get_serializer_class(self):
class ProfileViewset(viewsets.ModelViewSet):
serializer_class = ProfileSerializer
authentication_classes = (TokenAuthentication,)
permission_classes = (IsAuthenticated,)
permission_classes = (IsAuthenticated, DenyGuestUserMutationPermission)

def get_queryset(self):
return Profile.objects.filter(user=self.request.user)
Expand All @@ -879,16 +880,12 @@ def get_queryset(self):
class UserViewset(viewsets.ModelViewSet):
serializer_class = UserSerializer
authentication_classes = (TokenAuthentication,)
permission_classes = (IsAuthenticated,)
permission_classes = [IsAuthenticated, DenyGuestUserMutationPermission]

def get_queryset(self):
return User.objects.filter(pk=self.request.user.pk)

@action(
detail=False,
url_path="me",
serializer_class=UserMeSerializer,
)
@action(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 All @@ -915,7 +912,7 @@ class FieldReportViewset(ReadOnlyVisibilityViewsetMixin, viewsets.ModelViewSet):
) # for /docs
ordering_fields = ("summary", "event", "dtype", "created_at", "updated_at")
filterset_class = FieldReportFilter
authentication_class = [IsAuthenticated]
permission_classes = [IsAuthenticated, DenyGuestUserMutationPermission]
queryset = FieldReport.objects.select_related("dtype", "event").prefetch_related(
"actions_taken",
"actions_taken__actions",
Expand Down Expand Up @@ -1318,7 +1315,7 @@ class UsersViewset(viewsets.ReadOnlyModelViewSet):
"""

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

def get_queryset(self):
Expand Down Expand Up @@ -1356,7 +1353,7 @@ def get(self, _):

class ExportViewSet(viewsets.ModelViewSet):
serializer_class = ExportSerializer
permission_classes = [IsAuthenticated]
permission_classes = [IsAuthenticated, DenyGuestUserMutationPermission]

def get_queryset(self):
user = self.request.user
Expand Down
22 changes: 22 additions & 0 deletions api/migrations/0212_profile_limit_access_to_guest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Generated by Django 4.2.13 on 2024-07-30 07:53

from django.db import migrations, models


class Migration(migrations.Migration):

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

operations = [
migrations.AddField(
model_name="profile",
name="limit_access_to_guest",
field=models.BooleanField(
default=True,
help_text="If this value is set to true, the user is treated as a guest user regardless of any other permissions they may have, thereby depriving them of all non-guest user permissions.",
verbose_name="limit access to guest user permissions",
),
),
]
8 changes: 8 additions & 0 deletions api/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1853,6 +1853,14 @@ class OrgTypes(models.TextChoices):
phone_number = models.CharField(verbose_name=_("phone number"), blank=True, null=True, max_length=100)
last_frontend_login = models.DateTimeField(verbose_name=_("last frontend login"), null=True, blank=True)
accepted_montandon_license_terms = models.BooleanField(verbose_name=_("has accepted montandon license terms?"), default=False)
limit_access_to_guest = models.BooleanField(
help_text=(
"If this value is set to true, the user is treated as a guest user regardless of any other permissions"
" they may have, thereby depriving them of all non-guest user permissions."
),
verbose_name=_("limit access to guest user permissions"),
default=True,
)

class Meta:
verbose_name = _("user profile")
Expand Down
2 changes: 2 additions & 0 deletions api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1703,6 +1703,7 @@ class UserMeSerializer(UserSerializer):
is_per_admin_for_regions = serializers.SerializerMethodField()
is_per_admin_for_countries = serializers.SerializerMethodField()
user_countries_regions = serializers.SerializerMethodField()
limit_access_to_guest = serializers.BooleanField(read_only=True, source="profile.limit_access_to_guest")

class Meta:
model = User
Expand All @@ -1714,6 +1715,7 @@ class Meta:
"is_per_admin_for_regions",
"is_per_admin_for_countries",
"user_countries_regions",
"limit_access_to_guest",
)

@staticmethod
Expand Down
20 changes: 10 additions & 10 deletions api/snapshots/snap_test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"countries": [],
"countries_for_preview": [],
"created_at": "2008-01-01T00:00:00.123456Z",
"disaster_start_date": "2015-04-21T17:45:23.476445Z",
"disaster_start_date": "2021-09-20T13:28:12.297843Z",
"districts": [],
"dtype": 1,
"emergency_response_contact_email": None,
Expand Down Expand Up @@ -56,12 +56,12 @@
},
],
"field_reports": [],
"glide": "xJKxDZJiNfetzTUEHA",
"glide": "bxJKxDZJiNfetzTUEH",
"hide_attached_field_reports": True,
"hide_field_report_map": True,
"id": 2,
"ifrc_severity_level": 0,
"ifrc_severity_level_display": "Yellow",
"ifrc_severity_level": 1,
"ifrc_severity_level_display": "Orange",
"is_featured": False,
"is_featured_region": True,
"key_figures": [],
Expand All @@ -71,7 +71,7 @@
"parent_event": 1,
"response_activity_count": 0,
"slug": "ygwwmqzcudihyfjsonxkmtecqoxsfogyrdoxkxwnqrsrpemoki",
"summary": "NMGyDLJYVcCZKPmuMEGjdCgZvTfGPlcpTCCHHNkxxsyAXvRMdYOPvevgJRysqUQMjvfLQjwtPSQziMTftJyPYviQSVRHfPQBGxbxtlnvXFmoijesYgGXIVHcQvXNiMyjklSXNZkUCcAxRUpCNsWVYCoIptZYEmxRKCDXsXyGHAkmZMiqdPExJgTHhsfWkrCGjBfoCwbAdzGxpyfxobugTPvYjicsESiWTECNafbqnjJUMHBhXspthdpAOYNDehFMIbOGKpTjsBaNwpKAlQQfHxeHIGYGJbyEcOyxqVbwYewpUQOgXLVWvicwIvPlXRDSEOlZieTXDcsmcYmcutGzIEqcWPmswXdPvrhZxBzVCyvlFSFxZHrZfUBfBMlIsugfuQstCMTBkSCwCcUwNBrOYdeQOzxGZVRkbjMRYCciepXPxxyKcMjRCxxCWeKiHxzuPrphbVlFHyJhqXqTCnNsSFmhieClTCfZRuQwTeJIstkTTSOlYxGo",
"summary": "fNMGyDLJYVcCZKPmuMEGjdCgZvTfGPlcpTCCHHNkxxsyAXvRMdYOPvevgJRysqUQMjvfLQjwtPSQziMTftJyPYviQSVRHfPQBGxbxtlnvXFmoijesYgGXIVHcQvXNiMyjklSXNZkUCcAxRUpCNsWVYCoIptZYEmxRKCDXsXyGHAkmZMiqdPExJgTHhsfWkrCGjBfoCwbAdzGxpyfxobugTPvYjicsESiWTECNafbqnjJUMHBhXspthdpAOYNDehFMIbOGKpTjsBaNwpKAlQQfHxeHIGYGJbyEcOyxqVbwYewpUQOgXLVWvicwIvPlXRDSEOlZieTXDcsmcYmcutGzIEqcWPmswXdPvrhZxBzVCyvlFSFxZHrZfUBfBMlIsugfuQstCMTBkSCwCcUwNBrOYdeQOzxGZVRkbjMRYCciepXPxxyKcMjRCxxCWeKiHxzuPrphbVlFHyJhqXqTCnNsSFmhieClTCfZRuQwTeJIstkTTSOlYxG",
"tab_one_title": "cPXKqPnXKANObFOIsPtEpZZRztDeSdkCAEDnvMjuTuUwziWxGJ",
"tab_three_title": "gBiqUxWzxczdKJmxJseyGCWJrNRNhigzxYvJxWjmMGzGccciTv",
"tab_two_title": "gupDhrCpjgdsyNApkuKUumWkFGDFtFbfzGDpnLwddsFMPREsIa",
Expand All @@ -88,18 +88,18 @@
"countries": [],
"countries_for_preview": [],
"created_at": "2008-01-01T00:00:00.123456Z",
"disaster_start_date": "2015-04-21T17:45:23.476445Z",
"disaster_start_date": "2021-09-20T13:28:12.297843Z",
"districts": [],
"dtype": 1,
"emergency_response_contact_email": None,
"featured_documents": [],
"field_reports": [],
"glide": "xJKxDZJiNfetzTUEHA",
"glide": "bxJKxDZJiNfetzTUEH",
"hide_attached_field_reports": True,
"hide_field_report_map": True,
"id": 2,
"ifrc_severity_level": 0,
"ifrc_severity_level_display": "Yellow",
"ifrc_severity_level": 1,
"ifrc_severity_level_display": "Orange",
"is_featured": False,
"is_featured_region": True,
"key_figures": [],
Expand Down Expand Up @@ -145,7 +145,7 @@
"parent_event": 1,
"response_activity_count": 0,
"slug": "ygwwmqzcudihyfjsonxkmtecqoxsfogyrdoxkxwnqrsrpemoki",
"summary": "NMGyDLJYVcCZKPmuMEGjdCgZvTfGPlcpTCCHHNkxxsyAXvRMdYOPvevgJRysqUQMjvfLQjwtPSQziMTftJyPYviQSVRHfPQBGxbxtlnvXFmoijesYgGXIVHcQvXNiMyjklSXNZkUCcAxRUpCNsWVYCoIptZYEmxRKCDXsXyGHAkmZMiqdPExJgTHhsfWkrCGjBfoCwbAdzGxpyfxobugTPvYjicsESiWTECNafbqnjJUMHBhXspthdpAOYNDehFMIbOGKpTjsBaNwpKAlQQfHxeHIGYGJbyEcOyxqVbwYewpUQOgXLVWvicwIvPlXRDSEOlZieTXDcsmcYmcutGzIEqcWPmswXdPvrhZxBzVCyvlFSFxZHrZfUBfBMlIsugfuQstCMTBkSCwCcUwNBrOYdeQOzxGZVRkbjMRYCciepXPxxyKcMjRCxxCWeKiHxzuPrphbVlFHyJhqXqTCnNsSFmhieClTCfZRuQwTeJIstkTTSOlYxGo",
"summary": "fNMGyDLJYVcCZKPmuMEGjdCgZvTfGPlcpTCCHHNkxxsyAXvRMdYOPvevgJRysqUQMjvfLQjwtPSQziMTftJyPYviQSVRHfPQBGxbxtlnvXFmoijesYgGXIVHcQvXNiMyjklSXNZkUCcAxRUpCNsWVYCoIptZYEmxRKCDXsXyGHAkmZMiqdPExJgTHhsfWkrCGjBfoCwbAdzGxpyfxobugTPvYjicsESiWTECNafbqnjJUMHBhXspthdpAOYNDehFMIbOGKpTjsBaNwpKAlQQfHxeHIGYGJbyEcOyxqVbwYewpUQOgXLVWvicwIvPlXRDSEOlZieTXDcsmcYmcutGzIEqcWPmswXdPvrhZxBzVCyvlFSFxZHrZfUBfBMlIsugfuQstCMTBkSCwCcUwNBrOYdeQOzxGZVRkbjMRYCciepXPxxyKcMjRCxxCWeKiHxzuPrphbVlFHyJhqXqTCnNsSFmhieClTCfZRuQwTeJIstkTTSOlYxG",
"tab_one_title": "cPXKqPnXKANObFOIsPtEpZZRztDeSdkCAEDnvMjuTuUwziWxGJ",
"tab_three_title": "gBiqUxWzxczdKJmxJseyGCWJrNRNhigzxYvJxWjmMGzGccciTv",
"tab_two_title": "gupDhrCpjgdsyNApkuKUumWkFGDFtFbfzGDpnLwddsFMPREsIa",
Expand Down
164 changes: 160 additions & 4 deletions api/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,162 @@
EventFeaturedDocumentFactory,
EventLinkFactory,
)
from api.models import Profile
from deployments.factories.user import UserFactory
from main.test_case import APITestCase, SnapshotTestCase


class GuestUserPermissionTest(APITestCase):
def setUp(self):
# Create guest user
self.guest_user = User.objects.create(username="guest")
guest_profile = Profile.objects.get(user=self.guest_user)
guest_profile.limit_access_to_guest = True
guest_profile.save()

# Create go user
self.go_user = User.objects.create(username="go-user")
go_user_profile = Profile.objects.get(user=self.go_user)
go_user_profile.limit_access_to_guest = False
go_user_profile.save()

def test_guest_user_permission(self):
body = {}
guest_apis = [
"/api/v2/add_subscription/",
"/api/v2/del_subscription/",
"/api/v2/external-token/",
"/api/v2/user/me/",
]
id = 1 # NOTE: id is used just to test api that requires id, it doesnot indicate real id. It can be any number.
go_apis = [
"/api/v2/dref/",
"/api/v2/dref-final-report/",
f"/api/v2/dref-final-report/{id}/publish/",
"/api/v2/dref-op-update/",
f"/api/v2/dref-op-update/{id}/publish/",
"/api/v2/dref-share/",
f"/api/v2/dref/{id}/publish/",
"/api/v2/flash-update/",
"/api/v2/flash-update-file/multiple/",
"/api/v2/local-units/",
f"/api/v2/local-units/{id}/validate/",
"/api/v2/pdf-export/",
"/api/v2/per-assessment/",
"/api/v2/per-document-upload/",
"/api/v2/per-file/multiple/",
"/api/v2/per-prioritization/",
"/api/v2/per-work-plan/",
"/api/v2/project/",
"/api/v2/dref-files/",
"/api/v2/dref-files/multiple/",
"/api/v2/field-report/",
"/api/v2/flash-update-file/",
"/api/v2/per-file/",
"/api/v2/share-flash-update/",
"/api/v2/add_cronjob_log/",
"/api/v2/profile/",
"/api/v2/subscription/",
"/api/v2/user/",
]

get_apis = [
"/api/v2/dref/",
"/api/v2/dref-files/",
"/api/v2/dref-final-report/",
f"/api/v2/dref-final-report/{id}/",
"/api/v2/dref-op-update/",
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-overview/",
f"/api/v2/per-overview/{id}/",
"/api/v2/per-prioritization/",
f"/api/v2/per-prioritization/{id}/",
"/api/v2/per-work-plan/",
f"/api/v2/per-work-plan/{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/users/",
f"/api/v2/users/{id}/",
# Exports
f"/api/v2/export-flash-update/{1}/",
]

# NOTE: With custom Content Negotiation: Look for main.utils.SpreadSheetContentNegotiation
get_custom_negotiation_apis = [
f"/api/v2/export-per/{1}/",
]

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

self.authenticate(user=self.guest_user)

def _success_check(response): # NOTE: Only handles json responses
self.assertNotIn(response.status_code, [401, 403], response.content)
self.assertNotIn(response.json().get("error_code"), [401, 403], response.content)

def _failure_check(response, is_json=True):
self.assertIn(response.status_code, [401, 403], response.content)
if is_json:
self.assertIn(response.json()["error_code"], [401, 403], response.content)

for api_url in get_custom_negotiation_apis:
headers = {
"Accept": "text/html",
}
response = self.client.get(api_url, headers=headers, stream=True)
_failure_check(response, is_json=False)

# 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)
_failure_check(response)

# 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)
_failure_check(response)

# Guest user should be able to access guest apis
for api_url in guest_apis:
response = self.client.post(api_url, json=body)
_success_check(response)

# 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)
_success_check(response)

for api_url in get_apis:
response = self.client.get(api_url)
_success_check(response)


class AuthTokenTest(APITestCase):
def setUp(self):
user = User.objects.create(username="jo")
Expand Down Expand Up @@ -78,7 +231,7 @@ class FieldReportTest(APITestCase):
fixtures = ["DisasterTypes", "Actions"]

def test_create_and_update(self):
user = User.objects.create(username="jo")
user = UserFactory(username="jo")
region = models.Region.objects.create(name=1)
country1 = models.Country.objects.create(name="abc", region=region)
country2 = models.Country.objects.create(name="xyz")
Expand Down Expand Up @@ -204,21 +357,24 @@ def test_country_snippet_visibility(self):
self.assertEqual(response["count"], 0)

# perform the request with an authenticated user
user = User.objects.create(username="foo")
user = UserFactory(username="foo")
self.client.force_authenticate(user=user)
response = self.client.get("/api/v2/country_snippet/").json()
# one snippets available to anonymous user
self.assertEqual(response["count"], 1)

# perform the request with an ifrc user
user2 = User.objects.create(username="bar")
user2 = UserFactory(username="bar")
user2.user_permissions.add(self.ifrc_permission)
self.client.force_authenticate(user=user2)
response = self.client.get("/api/v2/country_snippet/").json()
self.assertEqual(response["count"], 2)

# perform the request with a superuser
super_user = User.objects.create_superuser(username="baz", email="[email protected]", password="12345678")
super_user = UserFactory(username="baz", email="[email protected]", password="12345678")
super_user.is_superuser = True
super_user.save()

self.client.force_authenticate(user=super_user)
response = self.client.get("/api/v2/country_snippet/").json()
self.assertEqual(response["count"], 2)
Expand Down
Loading

0 comments on commit ff82180

Please sign in to comment.