diff --git a/apps/entry/models.py b/apps/entry/models.py index c108e4c208..4ae3209e22 100644 --- a/apps/entry/models.py +++ b/apps/entry/models.py @@ -3,10 +3,6 @@ from django.contrib.postgres.aggregates.general import ArrayAgg from django.contrib.postgres.fields import ArrayField from django.db import models -from django.conf import settings -from django.urls import reverse -from django.utils.encoding import force_bytes -from django.utils.http import urlsafe_base64_encode from deep.middleware import get_current_user from unified_connector.models import ConnectorLeadPreviewAttachment @@ -25,7 +21,6 @@ Exportable, ) from assisted_tagging.models import DraftEntry -from gallery.enums import PrivateFileModuleType class EntryAttachment(models.Model): @@ -71,19 +66,6 @@ def clone_from_lead_attachment(cls, lead_attachment: LeadPreviewAttachment) -> ' entry_attachment.save() return entry_attachment - def get_file_url(self): - return '{protocol}://{domain}{url}'.format( - protocol=settings.HTTP_PROTOCOL, - domain=settings.DJANGO_API_HOST, - url=reverse( - 'external_private_url', - kwargs={ - 'module': PrivateFileModuleType.ENTRY_ATTACHMENT.value, - 'identifier': urlsafe_base64_encode(force_bytes(self.id)) - } - ) - ) - class Entry(UserResource, ProjectEntityMixin): """ diff --git a/apps/export/entries/excel_exporter.py b/apps/export/entries/excel_exporter.py index 6135239c3a..67ebee9d27 100644 --- a/apps/export/entries/excel_exporter.py +++ b/apps/export/entries/excel_exporter.py @@ -275,7 +275,7 @@ def add_entries_from_excel_data_for_static_column( return [entry_excerpt, entry.dropped_excerpt] return entry_excerpt elif exportable == Export.StaticColumn.LEAD_ENTRY_ENTRY_ATTACHMENT_FILE_PREVIEW: - return get_private_file_url(PrivateFileModuleType.ENTRY_ATTACHMENT.value, entry.entry_attachment.id) + return get_private_file_url(PrivateFileModuleType.ENTRY_ATTACHMENT, entry) def add_entries_from_excel_data(self, rows, data, export_data): export_type = data.get('type') diff --git a/apps/gallery/tests/test_apis.py b/apps/gallery/tests/test_apis.py index 8fe6cd7e7d..928ecde0af 100644 --- a/apps/gallery/tests/test_apis.py +++ b/apps/gallery/tests/test_apis.py @@ -1,20 +1,24 @@ import os import tempfile +from analysis_framework.factories import AnalysisFrameworkFactory from django.urls import reverse from django.conf import settings from django.utils.http import urlsafe_base64_encode from django.utils.encoding import force_bytes -from rest_framework import status +from entry.factories import EntryAttachmentFactory, EntryFactory +from lead.factories import LeadFactory from deep.tests import TestCase from gallery.models import File, FilePreview from lead.models import Lead from project.models import Project -from entry.models import Entry, EntryAttachment -from user.models import User +from entry.models import Entry +from user.factories import UserFactory +from project.factories import ProjectFactory from gallery.enums import PrivateFileModuleType +from utils.graphene.tests import GraphQLTestCase class GalleryTests(TestCase): @@ -211,36 +215,99 @@ def save_file_with_api(self, kwargs={}): # NOTE: Test for files -class PrivateAttachmentFileViewTest(TestCase): +class PrivateAttachmentFileViewTest(GraphQLTestCase): def setUp(self): + super().setUp() # Create a test user - self.user = User.objects.create_user(username='testuser', password='testpassword') + self.member_user = UserFactory.create() + self.member_user1 = UserFactory.create() + self.normal_user = UserFactory.create() + self.af = AnalysisFrameworkFactory.create() # Create a test entry attachment - self.project = Project.objects.create() - self.lead = Lead.objects.create(project=Project) - self.attachment = EntryAttachment.objects.create() - self.entry = Entry.objects.create( - lead=self.lead, - project=self.project, - entry=self.attachment + # for normal user + self.project = ProjectFactory.create() + + # for member user + self.project1 = ProjectFactory.create() + self.project1.add_member(self.member_user, role=self.project_role_admin) + + self.project2 = ProjectFactory.create() + self.project2.add_member(self.member_user1, role=self.project_role_reader) + + self.lead = LeadFactory.create(project=self.project) + self.lead1 = LeadFactory.create(project=self.project1) + self.lead2 = LeadFactory.create(project=self.project2) + + self.attachment = EntryAttachmentFactory.create() + self.attachment1 = EntryAttachmentFactory.create() + self.attachment2 = EntryAttachmentFactory.create() + + self.entry = EntryFactory.create( + analysis_framework=self.af, + lead=self.lead2, + project=self.project1, + entry_attachment=self.attachment ) - self.url = reverse('private_attachment_file_view', kwargs={ + self.entry1 = EntryFactory.create( + analysis_framework=self.af, + lead=self.lead1, + project=self.project1, + entry_attachment=self.attachment1 + ) + + self.entry2 = EntryFactory.create( + analysis_framework=self.af, + lead=self.lead2, + project=self.project2, + entry_attachment=self.attachment2 + ) + + def test_without_login(self): + self.url = reverse('external_private_url', kwargs={ 'module': PrivateFileModuleType.ENTRY_ATTACHMENT.value, - 'identifier': urlsafe_base64_encode(force_bytes(self.attachment.id)), + 'identifier': urlsafe_base64_encode(force_bytes(self.entry1.id)), + 'filename': "test.pdf" }) + response = self.client.get(self.url) + self.assertEqual(401, response.status_code) - def test_access_by_authenticated_user(self): - self.authenticate() + def test_access_by_normal_user(self): + self.url = reverse('external_private_url', kwargs={ + 'module': PrivateFileModuleType.ENTRY_ATTACHMENT.value, + 'identifier': urlsafe_base64_encode(force_bytes(self.entry1.id)), + 'filename': "test.pdf" + }) + self.force_login(self.normal_user) response = self.client.get(self.url) - self.assert_200(response) + self.assertEqual(403, response.status_code) + + def test_access_by_non_member_user(self): + self.url = reverse('external_private_url', kwargs={ + 'module': PrivateFileModuleType.ENTRY_ATTACHMENT.value, + 'identifier': urlsafe_base64_encode(force_bytes(self.entry2.id)), + 'filename': "test.pdf" + }) + self.force_login(self.member_user) + response = self.client.get(self.url) + self.assertEqual(403, response.status_code) + + def test_access_by_member_user(self): + self.url = reverse('external_private_url', kwargs={ + 'module': PrivateFileModuleType.ENTRY_ATTACHMENT.value, + 'identifier': urlsafe_base64_encode(force_bytes(self.entry1.id)), + 'filename': "test.pdf" + }) + self.force_login(self.member_user) + response = self.client.get(self.url) + self.assertEqual(302, response.status_code) def test_access_forbidden(self): - self.authenticate() - invalid_url = reverse('private_attachment_file_view', kwargs={ + self.force_login(self.member_user) + invalid_url = reverse('external_private_url', kwargs={ 'module': PrivateFileModuleType.ENTRY_ATTACHMENT.value, 'identifier': urlsafe_base64_encode(force_bytes(999999)), + 'filename': 'test.pdf' }) response = self.client.get(invalid_url) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - self.assertEqual(response.data['error'], "Access Forbidden Or File doesn't exists, Contact Admin") + self.assertEqual(404, response.status_code) diff --git a/apps/gallery/utils.py b/apps/gallery/utils.py index 73887ef9e7..89d16f7d18 100644 --- a/apps/gallery/utils.py +++ b/apps/gallery/utils.py @@ -3,16 +3,34 @@ from django.utils.encoding import force_bytes from django.utils.http import urlsafe_base64_encode +from gallery.enums import PrivateFileModuleType +from deep.permissions import ProjectPermissions as PP +from lead.models import Lead -def get_private_file_url(obj, id): + +def get_private_file_url(private_file_module_type: PrivateFileModuleType, entry): return '{protocol}://{domain}{url}'.format( protocol=settings.HTTP_PROTOCOL, domain=settings.DJANGO_API_HOST, url=reverse( 'external_private_url', kwargs={ - 'module': obj, - 'identifier': urlsafe_base64_encode(force_bytes(id)) + 'module': private_file_module_type.value, + 'identifier': urlsafe_base64_encode(force_bytes(entry.id)), + 'filename': entry.entry_attachment.file.name } ) ) + + +def check_private_entry_qs(user, project, level): + permission = PP.get_permissions(project, user) + if PP.Permission.VIEW_ENTRY in permission: + if PP.Permission.VIEW_ALL_LEAD in permission: + return True + elif PP.Permission.VIEW_ONLY_UNPROTECTED_LEAD in permission: + return True + elif level == Lead.Confidentiality.UNPROTECTED: + return True + else: + return False diff --git a/apps/gallery/views.py b/apps/gallery/views.py index b506d9e48f..94a01acfb0 100644 --- a/apps/gallery/views.py +++ b/apps/gallery/views.py @@ -7,7 +7,8 @@ from django.utils.http import urlsafe_base64_decode from django.shortcuts import redirect, get_object_or_404 -from gallery.enums import PrivateFileModuleType +from gallery.enums import PrivateFileModuleType +from gallery.utils import check_private_entry_qs from rest_framework import ( views, viewsets, @@ -25,7 +26,7 @@ from deep.permalinks import Permalink from project.models import Project from lead.models import Lead -from entry.models import Entry, EntryAttachment +from entry.models import Entry from user_resource.filters import UserResourceFilterSet from utils.extractor.formats import ( @@ -86,13 +87,21 @@ def get(self, request, uuid=None, filename=None): class PrivateAttachmentFileView(views.APIView): permission_classes = [permissions.IsAuthenticated] - def get(self, request, module=None, identifier=None): + def get(self, request, module=None, identifier=None, filename=None): id = force_text(urlsafe_base64_decode(identifier)) user = request.user obj = None if module == PrivateFileModuleType.ENTRY_ATTACHMENT.value: - obj = get_object_or_404(EntryAttachment, id=id) - obj.entry.get_for(user) + entry = get_object_or_404(Entry, id=id) + if not entry.project.is_member(user): + return response.Response({ + 'error': 'Unauthorized for the content' + }, status.HTTP_403_FORBIDDEN) + if not check_private_entry_qs(user, entry.project, entry.lead.confidentiality): + return response.Response({ + 'error': 'Access Denied' + }, status.HTTP_403_FORBIDDEN) + obj = entry.entry_attachment if obj: return redirect(request.build_absolute_uri(obj.file.url)) return response.Response({ @@ -134,6 +143,7 @@ class PublicFileView(View): """ NOTE: Public File API is deprecated. """ + def get(self, request, fidb64=None, token=None, filename=None): file_id = force_text(urlsafe_base64_decode(fidb64)) file = get_object_or_404(File, id=file_id) diff --git a/deep/urls.py b/deep/urls.py index 6b28a9ffba..80fe97464e 100644 --- a/deep/urls.py +++ b/deep/urls.py @@ -437,7 +437,7 @@ def get_api_path(path): name='deprecated_gallery_private_url', ), path( - 'external/private-file//', + 'external/private-file///', PrivateAttachmentFileView.as_view(), name='external_private_url', ),