Skip to content
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

Create Guest User Permission with Public Content Access #2183

Merged
merged 3 commits into from
Aug 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
thenav56 marked this conversation as resolved.
Show resolved Hide resolved
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)
susilnem marked this conversation as resolved.
Show resolved Hide resolved

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", "countries", "districts", "regions"
)
Expand Down Expand Up @@ -1308,7 +1305,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 @@ -1346,7 +1343,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 @@ -1850,6 +1850,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